From 0b04223626bdeab98f9abf0f6db997abd25f745d Mon Sep 17 00:00:00 2001 From: uchouT Date: Mon, 27 Apr 2026 22:19:22 +0800 Subject: [PATCH 1/3] feat(core): align stello_create_session schema with EngineForkOptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add context: 'compress' to enum (surfaces existing engine capability) - Add skills field with three-state semantics - Rename vars → profileVars (drops the internal mapping layer) --- .../__tests__/create-session-tool.test.ts | 47 +++++++++++++++++-- .../src/builtin-tools/create-session-tool.ts | 35 +++++++------- 2 files changed, 61 insertions(+), 21 deletions(-) diff --git a/packages/core/src/builtin-tools/__tests__/create-session-tool.test.ts b/packages/core/src/builtin-tools/__tests__/create-session-tool.test.ts index 5303806..6d29422 100644 --- a/packages/core/src/builtin-tools/__tests__/create-session-tool.test.ts +++ b/packages/core/src/builtin-tools/__tests__/create-session-tool.test.ts @@ -10,7 +10,17 @@ describe('createSessionTool factory', () => { expect(tool.parameters).toBeTypeOf('object') }) - it('execute calls ctx.agent.forkSession with mapped options', async () => { + it('schema exposes context: compress, profileVars, and skills', () => { + const tool = createSessionTool() + const props = (tool.parameters as { properties: Record }).properties + expect(props.context.enum).toEqual(['none', 'inherit', 'compress']) + expect(props.profileVars).toBeDefined() + expect(props.skills).toBeDefined() + expect(props.skills.type).toBe('array') + expect(props.vars).toBeUndefined() + }) + + it('execute forwards all new fields to forkSession', async () => { const forkSpy = vi.fn().mockResolvedValue({ id: 'child-1', label: 'Child' }) const ctx: ToolExecutionContext = { agent: { forkSession: forkSpy, profiles: undefined } as never, @@ -19,17 +29,44 @@ describe('createSessionTool factory', () => { } const tool = createSessionTool() const result = await tool.execute( - { label: 'Child', systemPrompt: 'sp', context: 'inherit' }, + { + label: 'Child', + systemPrompt: 'sp', + context: 'compress', + profileVars: { region: 'NA' }, + skills: ['a', 'b'], + }, ctx, ) - expect(forkSpy).toHaveBeenCalledWith('parent-1', expect.objectContaining({ + expect(forkSpy).toHaveBeenCalledWith('parent-1', { label: 'Child', systemPrompt: 'sp', - context: 'inherit', - })) + context: 'compress', + profileVars: { region: 'NA' }, + skills: ['a', 'b'], + }) expect(result).toEqual({ success: true, data: { sessionId: 'child-1', label: 'Child' } }) }) + it('execute preserves three-state skills semantics', async () => { + const forkSpy = vi.fn().mockResolvedValue({ id: 'c', label: 'L' }) + const ctx: ToolExecutionContext = { + agent: { forkSession: forkSpy, profiles: undefined } as never, + sessionId: 'p', + toolName: 'stello_create_session', + } + const tool = createSessionTool() + + await tool.execute({ label: 'L' }, ctx) + expect(forkSpy.mock.calls[0][1]).not.toHaveProperty('skills') + + await tool.execute({ label: 'L', skills: [] }, ctx) + expect(forkSpy.mock.calls[1][1].skills).toEqual([]) + + await tool.execute({ label: 'L', skills: ['a'] }, ctx) + expect(forkSpy.mock.calls[2][1].skills).toEqual(['a']) + }) + it('returns error for unknown profile', async () => { const profilesStub = { has: (n: string) => n === 'known' } const ctx: ToolExecutionContext = { diff --git a/packages/core/src/builtin-tools/create-session-tool.ts b/packages/core/src/builtin-tools/create-session-tool.ts index 0b62e92..5890489 100644 --- a/packages/core/src/builtin-tools/create-session-tool.ts +++ b/packages/core/src/builtin-tools/create-session-tool.ts @@ -7,9 +7,10 @@ const DESCRIPTION = `创建一个新的子会话,从当前会话派生。子 - label(必填): 子会话显示名 - systemPrompt(可选): 子会话的系统提示词 - prompt(可选): 子会话开场消息(assistant 首条消息) -- context(可选): 'none' | 'inherit' — 是否继承父会话对话历史 +- context(可选): 'none' | 'inherit' | 'compress' — 上下文继承策略(none=空启动 / inherit=继承父对话 / compress=父对话压缩入 systemPrompt) - profile(可选): 引用预注册的 ForkProfile 名(运行时由 agent 动态校验) -- vars(可选): 提供给 profile.systemPromptFn 的模板变量` +- profileVars(可选): 提供给 profile.systemPromptFn 的模板变量 +- skills(可选): Skill 白名单。三态语义:不传=继承父 / [] =禁用 / ["a","b"]=只允许这几个` const PARAMETERS = { type: 'object', @@ -19,29 +20,24 @@ const PARAMETERS = { prompt: { type: 'string', description: '子会话开场消息' }, context: { type: 'string', - enum: ['none', 'inherit'], - description: '是否继承父会话对话历史', + enum: ['none', 'inherit', 'compress'], + description: 'none=空启动 / inherit=继承父对话 / compress=父对话压缩入 systemPrompt', }, profile: { type: 'string', description: '预注册 ForkProfile 名(运行时校验)' }, - vars: { + profileVars: { type: 'object', description: 'profile.systemPromptFn 的模板变量', additionalProperties: { type: 'string' }, }, + skills: { + type: 'array', + items: { type: 'string' }, + description: 'Skill 白名单。三态:不传=继承 / [] =禁用 / ["a","b"]=只允许这几个', + }, }, required: ['label'], } -function mapArgsToForkOptions(args: Record): EngineForkOptions { - const options: EngineForkOptions = { label: args.label as string } - if (args.systemPrompt !== undefined) options.systemPrompt = args.systemPrompt as string - if (args.prompt !== undefined) options.prompt = args.prompt as string - if (args.context !== undefined) options.context = args.context as 'none' | 'inherit' - if (args.profile !== undefined) options.profile = args.profile as string - if (args.vars !== undefined) options.profileVars = args.vars as Record - return options -} - export function createSessionTool(): ToolRegistryEntry { return { name: 'stello_create_session', @@ -53,7 +49,14 @@ export function createSessionTool(): ToolRegistryEntry { return { success: false, error: `Profile "${profileName}" 未注册` } } try { - const child = await ctx.agent.forkSession(ctx.sessionId, mapArgsToForkOptions(args)) + const options: EngineForkOptions = { label: args.label as string } + if (args.systemPrompt !== undefined) options.systemPrompt = args.systemPrompt as string + if (args.prompt !== undefined) options.prompt = args.prompt as string + if (args.context !== undefined) options.context = args.context as 'none' | 'inherit' | 'compress' + if (args.profile !== undefined) options.profile = args.profile as string + if (args.profileVars !== undefined) options.profileVars = args.profileVars as Record + if (args.skills !== undefined) options.skills = args.skills as string[] + const child = await ctx.agent.forkSession(ctx.sessionId, options) return { success: true, data: { sessionId: child.id, label: child.label } } } catch (e) { return { success: false, error: e instanceof Error ? e.message : String(e) } From 4c43e9c89aa347eee72ab9f2bc5b5d6008845fdf Mon Sep 17 00:00:00 2001 From: uchouT Date: Mon, 27 Apr 2026 22:25:24 +0800 Subject: [PATCH 2/3] fix(core): satisfy strict typecheck in create-session-tool tests Narrow the schema-shape cast and the mock.calls accesses so the test file no longer regresses tsc --noEmit. --- .../__tests__/create-session-tool.test.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/core/src/builtin-tools/__tests__/create-session-tool.test.ts b/packages/core/src/builtin-tools/__tests__/create-session-tool.test.ts index 6d29422..b38b9d6 100644 --- a/packages/core/src/builtin-tools/__tests__/create-session-tool.test.ts +++ b/packages/core/src/builtin-tools/__tests__/create-session-tool.test.ts @@ -12,7 +12,14 @@ describe('createSessionTool factory', () => { it('schema exposes context: compress, profileVars, and skills', () => { const tool = createSessionTool() - const props = (tool.parameters as { properties: Record }).properties + const props = (tool.parameters as { + properties: { + context: { enum: string[] } + profileVars: object + skills: { type: string } + vars?: unknown + } + }).properties expect(props.context.enum).toEqual(['none', 'inherit', 'compress']) expect(props.profileVars).toBeDefined() expect(props.skills).toBeDefined() @@ -58,13 +65,15 @@ describe('createSessionTool factory', () => { const tool = createSessionTool() await tool.execute({ label: 'L' }, ctx) - expect(forkSpy.mock.calls[0][1]).not.toHaveProperty('skills') + expect(forkSpy.mock.calls[0]![1]).not.toHaveProperty('skills') await tool.execute({ label: 'L', skills: [] }, ctx) - expect(forkSpy.mock.calls[1][1].skills).toEqual([]) + const opts1 = forkSpy.mock.calls[1]![1] as { skills?: string[] } + expect(opts1.skills).toEqual([]) await tool.execute({ label: 'L', skills: ['a'] }, ctx) - expect(forkSpy.mock.calls[2][1].skills).toEqual(['a']) + const opts2 = forkSpy.mock.calls[2]![1] as { skills?: string[] } + expect(opts2.skills).toEqual(['a']) }) it('returns error for unknown profile', async () => { From f15b0b1e5d19866ccd799ae6599c8383ffa052e6 Mon Sep 17 00:00:00 2001 From: uchouT Date: Mon, 27 Apr 2026 22:28:27 +0800 Subject: [PATCH 3/3] chore: bump @stello-ai/core to 0.7.2 --- packages/core/CHANGELOG.md | 8 ++++++++ packages/core/package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index aed87ee..670e96b 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,13 @@ # @stello-ai/core +## 0.7.2 + +### Added +- `stello_create_session` tool schema: `context.enum` gains `'compress'` (surfaces existing engine capability); new `skills` field with three-state semantics (undefined=inherit / `[]`=disable / `['a','b']`=whitelist). + +### Changed +- `stello_create_session` tool: parameter `vars` renamed to `profileVars` for parity with `EngineForkOptions.profileVars`. The internal `mapArgsToForkOptions` mapping helper is removed (execute now does direct conditional passthrough). Wire-level only — no consumer code references the old name. + ## 0.7.1 ### Patch Changes diff --git a/packages/core/package.json b/packages/core/package.json index 9b5befe..2586456 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@stello-ai/core", - "version": "0.7.1", + "version": "0.7.2", "description": "The first open-source conversation topology engine", "license": "Apache-2.0", "author": "Stello Contributors",