Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,24 @@ 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: {
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()
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,
Expand All @@ -19,17 +36,46 @@ 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)
const opts1 = forkSpy.mock.calls[1]![1] as { skills?: string[] }
expect(opts1.skills).toEqual([])

await tool.execute({ label: 'L', skills: ['a'] }, ctx)
const opts2 = forkSpy.mock.calls[2]![1] as { skills?: string[] }
expect(opts2.skills).toEqual(['a'])
})

it('returns error for unknown profile', async () => {
const profilesStub = { has: (n: string) => n === 'known' }
const ctx: ToolExecutionContext = {
Expand Down
35 changes: 19 additions & 16 deletions packages/core/src/builtin-tools/create-session-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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<string, unknown>): 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<string, string>
return options
}

export function createSessionTool(): ToolRegistryEntry {
return {
name: 'stello_create_session',
Expand All @@ -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<string, string>
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) }
Expand Down