import { describe, expect, it } from 'vitest' import { Context } from 'cordis' import Loader from '@cordisjs/plugin-loader' import AgentRegistry, { agentEvents, AgentMessageId } from '@deepseek-ai/dsh-agent' import type { Agent, AgentStatus, InjectOptions } from '@deepseek-ai/dsh-agent' import GoalService, { GoalId } from '@deepseek-ai/dsh-goal' import type { GoalRef } from '@deepseek-ai/dsh-goal' import { CallId } from '@deepseek-ai/dsh-llm' import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm' import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import type { ToolExecutionResult } from '@deepseek-ai/dsh-tools' import * as toolGoal from '@deepseek-ai/dsh-tool-goal' const testToolSignal = new AbortController().signal interface StubAgent { readonly agent: Agent readonly session: Session setStatus(status: AgentStatus): void } /** Build one registry-compatible live agent whose injections append in place. */ function stubAgent(rawId: string, supplied?: Session): StubAgent { const session = supplied ?? new Session(SessionId(rawId)) let status: AgentStatus = 'running' const agent: Agent = { id: session.id, options: {}, session, get status() { return status }, ctx: new Context(), followup: () => AgentMessageId('stub'), queue: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject(content: ContentBlock[], options?: InjectOptions) { const source = options?.source ?? { kind: 'plugin', plugin: '' } session.append('user/message', { content, source, ...options?.meta === undefined ? {} : { meta: options.meta }, }, { surfaceOp: 'append' }) return AgentMessageId('stub') }, send: () => AgentMessageId('stub'), cancel() {}, whenIdle() { return Promise.resolve() }, } return { agent, session, setStatus(value) { status = value } } } /** Open one message-triggered turn with its accepted model-visible input. */ function openTurn(stub: StubAgent, source: MessageSource, text = 'prompt'): number { const turn = stub.session.events .filter(event => event.type === 'turn/start') .reduce((max, event) => Math.max(max, event.data.turn), 0) + 1 stub.session.append('turn/start', { turn, trigger: { kind: 'message', source } }) stub.session.append('user/message', { content: [{ type: 'text', text }], source, }, { surfaceOp: 'append' }) return turn } /** Close the currently open test turn. */ function closeTurn(stub: StubAgent, turn: number): void { stub.session.append('turn/end', { turn, reason: { kind: 'completed' } }) } async function harness(config: toolGoal.Config = {}) { const ctx = new Context() await ctx.plugin(SystemPrompt) await ctx.plugin(AgentRegistry) await ctx.plugin(ToolRegistry) await ctx.plugin(GoalService) const fiber = await ctx.plugin(toolGoal, config) const root = stubAgent(`goal-tool-root-${Math.random()}`) ctx.agents.register(root.agent) return { ctx, fiber, root } } /** Execute one registered tool under an optional driver initiator. */ async function execute( ctx: Context, name: string, args: unknown, agent?: Agent, initiator: Agent | undefined = agent, ): Promise { const run = () => ctx.tools.execute({ signal: testToolSignal, callId: CallId(`call-${Math.random()}`), name, arguments: args, ...agent === undefined ? {} : { agent }, }) return initiator === undefined ? run() : ctx.agents.withInitiator(initiator, run) } /** Parse the compact JSON returned by a successful goal tool. */ function resultJson(result: ToolExecutionResult): Record { expect(result.isError).toBe(false) if (result.isError) throw new Error('expected goal tool success') const block = result.content[0] if (block?.type !== 'text') throw new Error('expected text tool result') const parsed = JSON.parse(block.text) as Record expect(result.value).toEqual(parsed) return parsed } /** Read the returned goal sub-object. */ function resultGoal(result: ToolExecutionResult): Record { const goal = resultJson(result)['goal'] if (typeof goal !== 'object' || goal === null) throw new Error('expected returned goal') return goal as Record } describe('goal tool registration and presentation', () => { it('registers three exclusive tools plus configured guidance and disposes all contributions', async () => { const { ctx, fiber } = await harness({ blockedAfterConsecutiveRounds: 5 }) expect(['create_goal', 'get_goal', 'update_goal'].map(name => ctx.tools.get(name)?.name)) .toEqual(['create_goal', 'get_goal', 'update_goal']) for (const name of ['create_goal', 'get_goal', 'update_goal']) { expect(ctx.tools.executionMode({ signal: testToolSignal, callId: CallId(name), name, arguments: {} })) .toEqual({ kind: 'exclusive' }) } const section = (await ctx.systemPrompt.assemble()).sections.find(item => item.name === 'tool:goal') expect(section?.text).toContain('infer goal intent') expect(section?.text).toContain('at least 5 consecutive goal rounds') await fiber.dispose() expect(ctx.tools.get('get_goal')).toBeUndefined() expect((await ctx.systemPrompt.assemble()).sections.some(item => item.name === 'tool:goal')).toBe(false) }) it('uses args-only generic render intent and soft-fails malformed replay args', async () => { const { ctx } = await harness() expect(ctx.tools.get('get_goal')?.presentCall?.({})).toEqual({ card: 'generic', title: 'Read current goal', kind: 'read', }) expect(ctx.tools.get('create_goal')?.presentCall?.({ objective: 'ship' })).toEqual({ card: 'generic', title: 'Create goal', kind: 'other', rawInput: 'ship', }) expect(ctx.tools.get('update_goal')?.presentCall?.({ goal_id: 'goal-1', revision: 2, action: 'blocked', blocked_reason: 'Waiting for a human choice.', })).toEqual({ card: 'generic', title: 'Mark goal', kind: 'other', rawInput: 'Waiting for a human choice.' }) expect(ctx.tools.get('update_goal')?.presentCall?.({ goal_id: 'goal-1', revision: 2, action: 'edit', objective: 'ship', max_goal_rounds: 0, blocked_reason: '', })).toEqual({ card: 'generic', title: 'Edit goal', kind: 'other', rawInput: 'ship' }) expect(ctx.tools.get('update_goal')?.presentCall?.({ goal_id: 'goal-1', revision: 2, action: 'edit', objective: '', max_goal_rounds: 8, blocked_reason: '', })).toEqual({ card: 'generic', title: 'Edit goal', kind: 'other', rawInput: 8 }) expect(ctx.tools.get('update_goal')?.presentCall?.({ goal_id: 'goal-1', revision: 2, action: 'resume', objective: '', max_goal_rounds: 0, blocked_reason: '', })).toEqual({ card: 'generic', title: 'Resume goal', kind: 'other', rawInput: 'goal-1' }) expect(ctx.tools.get('update_goal')?.presentCall?.({ wrong: true })).toBeUndefined() }) it('has the Loader-safe namespace export shape', () => { expect('default' in toolGoal).toBe(false) expect(toolGoal.name).toBe('tool-goal') expect(toolGoal.inject).toEqual(['agents', 'goals', 'tools', 'systemPrompt']) const loader = Object.create(Loader.prototype) as Loader expect(loader.unwrapExports(toolGoal)).toBe(toolGoal) }) it('fails invalid direct config before registering anything', async () => { const ctx = new Context() await ctx.plugin(SystemPrompt) await ctx.plugin(AgentRegistry) await ctx.plugin(ToolRegistry) await ctx.plugin(GoalService) expect(() => { toolGoal.apply(ctx, { blockedAfterConsecutiveRounds: 1.5 }) }).toThrow( 'blockedAfterConsecutiveRounds must be a positive safe integer', ) expect(ctx.tools.get('get_goal')).toBeUndefined() }) it('resolves the direct-apply default before registration', async () => { const ctx = new Context() await ctx.plugin(SystemPrompt) await ctx.plugin(AgentRegistry) await ctx.plugin(ToolRegistry) await ctx.plugin(GoalService) toolGoal.apply(ctx, {}) const section = (await ctx.systemPrompt.assemble()).sections.find(item => item.name === 'tool:goal') expect(section?.text).toContain('at least 3 consecutive goal rounds') }) }) describe('goal tool execution authority', () => { it('lets a root model infer create intent from its accepted human turn', async () => { const { ctx, root } = await harness() openTurn(root, { kind: 'user' }, '请持续工作直到这个功能完成') const result = await execute(ctx, 'create_goal', { objective: 'Finish the feature', max_goal_rounds: 9, }, root.agent) expect(resultGoal(result)).toMatchObject({ objective: 'Finish the feature', revision: 1, phase: 'active', maxGoalRounds: 9, }) expect(resultJson(result)['activation']).toBe('armed') expect(ctx.goals.get(root.agent)?.objective).toBe('Finish the feature') }) it('rejects agentless, driverless, non-human, and live-child creation', async () => { const { ctx, root } = await harness() const agentless = await execute(ctx, 'get_goal', {}) expect(agentless.error?.info?.code).toBe('GOAL_TOOL_AGENT_REQUIRED') openTurn(root, { kind: 'user' }) const driverless = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('call-driverless'), name: 'get_goal', arguments: {}, agent: root.agent, }) expect(driverless.error?.info?.code).toBe('GOAL_TOOL_DRIVER_REQUIRED') closeTurn(root, 1) openTurn(root, { kind: 'plugin', plugin: 'test' }) const nonHuman = await execute(ctx, 'create_goal', { objective: 'forged' }, root.agent) expect(nonHuman.error?.info?.code).toBe('GOAL_TOOL_AUTHORITY_REQUIRED') closeTurn(root, 2) const child = stubAgent('goal-tool-child') ctx.agents.enter(child.agent, root.agent) ctx.agents.announce(child.agent) openTurn(child, { kind: 'user' }) const childResult = await execute(ctx, 'create_goal', { objective: 'child goal' }, child.agent) expect(childResult.error?.info?.code).toBe('GOAL_TOOL_AUTHORITY_REQUIRED') }) it('rejects stale agent objects and agents outside running status through the executor', async () => { const { ctx, root } = await harness() openTurn(root, { kind: 'user' }) // A distinct agent object over root's exact session: same id, not the live // registered instance, so the executor must reject it. const stale = stubAgent('goal-tool-stale', root.agent.session).agent const staleResult = await execute(ctx, 'get_goal', {}, stale, stale) expect(staleResult.error?.info?.code).toBe('GOAL_TOOL_DRIVER_REQUIRED') root.setStatus('idle') const idleResult = await execute(ctx, 'get_goal', {}, root.agent) expect(idleResult.error?.info?.code).toBe('GOAL_TOOL_DRIVER_REQUIRED') }) it('treats a fork resumed as a runtime root as direct-human authority', async () => { const { ctx, root } = await harness() const originalTurn = openTurn(root, { kind: 'user' }) const created = ctx.goals.create(root.agent, { objective: 'resume the fork' }) closeTurn(root, originalTurn) const forkId = SessionId('goal-tool-resumed-fork') const forkSession = new Session(forkId, root.session.events, { version: SESSION_FORMAT_VERSION, id: forkId, createdAt: Date.now(), parentSession: root.session.id, seedLength: root.session.seq, }) const fork = stubAgent(forkId, forkSession) ctx.agents.register(fork.agent) expect(ctx.goals.get(fork.agent)).toMatchObject({ id: created.id, activation: 'disarmed' }) openTurn(fork, { kind: 'user' }, '继续这个目标') const resumed = await execute(ctx, 'update_goal', { goal_id: created.id, revision: created.revision, action: 'resume', }, fork.agent) expect(resultGoal(resumed)).toMatchObject({ id: created.id, revision: 2, phase: 'active' }) }) it('rejects calls before a turn and after its end boundary', async () => { const { ctx, root } = await harness() const before = await execute(ctx, 'get_goal', {}, root.agent) expect(before.error?.info?.code).toBe('GOAL_TOOL_DRIVER_REQUIRED') const turn = openTurn(root, { kind: 'user' }) closeTurn(root, turn) const after = await execute(ctx, 'get_goal', {}, root.agent) expect(after.error?.info?.code).toBe('GOAL_TOOL_DRIVER_REQUIRED') }) it('rejects terminal reporting without human input or a current goal round', async () => { const { ctx, root } = await harness() openTurn(root, { kind: 'plugin', plugin: 'test' }) const result = await execute(ctx, 'update_goal', { goal_id: 'goal-missing', revision: 1, action: 'complete', }, root.agent) expect(result.error?.info?.code).toBe('GOAL_TOOL_AUTHORITY_REQUIRED') const malformed = await execute(ctx, 'update_goal', { goal_id: 'goal-missing', revision: 1, action: 'pause', objective: 'probe', }, root.agent) expect(malformed.error?.info?.code).toBe('GOAL_TOOL_AUTHORITY_REQUIRED') }) it('accepts direct human steering in a goal-sourced root turn', async () => { const { ctx, root } = await harness() const humanTurn = openTurn(root, { kind: 'user' }) const created = ctx.goals.create(root.agent, { objective: 'steer me' }) closeTurn(root, humanTurn) const round = openTurn(root, { kind: 'goal', goalId: created.id, revision: created.revision, round: 1, }) root.session.append('steering/message', { turn: round, content: [{ type: 'text', text: 'pause now' }], source: { kind: 'user' }, }, { surfaceOp: 'append' }) const paused = await execute(ctx, 'update_goal', { goal_id: created.id, revision: created.revision, action: 'pause', }, root.agent) expect(resultGoal(paused)).toMatchObject({ phase: 'paused', revision: 2 }) }) it('rejects an initiator different from exec.agent', async () => { const { ctx, root } = await harness() const other = stubAgent('goal-tool-other') ctx.agents.register(other.agent) openTurn(other, { kind: 'user' }) const result = await execute(ctx, 'get_goal', {}, other.agent, root.agent) expect(result.error?.info?.code).toBe('GOAL_TOOL_DRIVER_REQUIRED') }) }) describe('goal tool state transitions', () => { it('reads null, then edits, pauses, and resumes by exact revision in one human turn', async () => { const { ctx, root } = await harness() openTurn(root, { kind: 'user' }) expect(resultJson(await execute(ctx, 'get_goal', {}, root.agent))).toEqual({ goal: null }) let goal = resultGoal(await execute(ctx, 'create_goal', { objective: 'old' }, root.agent)) goal = resultGoal(await execute(ctx, 'update_goal', { goal_id: goal['id'], revision: goal['revision'], action: 'edit', objective: 'new', max_goal_rounds: 8, }, root.agent)) expect(goal).toMatchObject({ objective: 'new', revision: 2, maxGoalRounds: 8 }) goal = resultGoal(await execute(ctx, 'update_goal', { goal_id: goal['id'], revision: goal['revision'], action: 'pause', }, root.agent)) expect(goal).toMatchObject({ phase: 'paused', revision: 3 }) goal = resultGoal(await execute(ctx, 'update_goal', { goal_id: goal['id'], revision: goal['revision'], action: 'resume', }, root.agent)) expect(goal).toMatchObject({ phase: 'active', revision: 4 }) expect(await agentEvents(ctx, root.agent).serial('agent/turn-stop', 1, testToolSignal)).toBeUndefined() }) it('terminal-stops an autonomous completion but leaves a human pause interactive', async () => { const { ctx, root } = await harness() const humanTurn = openTurn(root, { kind: 'user' }) const created = ctx.goals.create(root.agent, { objective: 'pause cleanly' }) const paused = await execute(ctx, 'update_goal', { goal_id: created.id, revision: created.revision, action: 'pause', }, root.agent) expect(resultGoal(paused)).toMatchObject({ phase: 'paused' }) expect(await agentEvents(ctx, root.agent).serial('agent/turn-stop', humanTurn, testToolSignal)).toBeUndefined() const resumed = resultGoal(await execute(ctx, 'update_goal', { goal_id: created.id, revision: 2, action: 'resume', }, root.agent)) closeTurn(root, humanTurn) const roundTurn = openTurn(root, { kind: 'goal', goalId: created.id, revision: resumed['revision'] as number, round: 1, }) const complete = await execute(ctx, 'update_goal', { goal_id: created.id, revision: resumed['revision'], action: 'complete', }, root.agent) expect(resultGoal(complete)).toMatchObject({ phase: 'complete' }) expect(await agentEvents(ctx, root.agent).serial('agent/turn-stop', roundTurn, testToolSignal)).toEqual({ action: 'stop' }) expect(await agentEvents(ctx, root.agent).serial('agent/turn-stop', roundTurn, testToolSignal)).toBeUndefined() }) it('rearms a restored active goal only after a new direct human prompt', async () => { const { ctx, root } = await harness() let turn = openTurn(root, { kind: 'user' }) const created = ctx.goals.create(root.agent, { objective: 'continue later' }) closeTurn(root, turn) agentEvents(ctx, root.agent).emit('agent/session-start', 'resume') expect(ctx.goals.get(root.agent)?.activation).toBe('disarmed') turn = openTurn(root, { kind: 'user' }, '继续') const resumed = await execute(ctx, 'update_goal', { goal_id: created.id, revision: created.revision, action: 'resume', }, root.agent) expect(resultGoal(resumed)).toMatchObject({ phase: 'active', revision: 2 }) expect(resultJson(resumed)['activation']).toBe('armed') closeTurn(root, turn) }) it('returns structured domain and conditional-argument failures', async () => { const { ctx, root } = await harness() openTurn(root, { kind: 'user' }) const invalidCreate = await execute(ctx, 'create_goal', { objective: ' ' }, root.agent) expect(invalidCreate.error?.info?.code).toBe('GOAL_INVALID_OBJECTIVE') const created = ctx.goals.create(root.agent, { objective: 'valid' }) const replacement = await execute(ctx, 'update_goal', { goal_id: created.id, revision: created.revision, action: 'pause', objective: 'not valid for pause', }, root.agent) expect(replacement.error?.info?.code).toBe('GOAL_TOOL_INVALID_UPDATE') const terminalUpdate = await execute(ctx, 'update_goal', { goal_id: created.id, revision: created.revision, action: 'complete', max_goal_rounds: 2, }, root.agent) expect(terminalUpdate.error?.info?.code).toBe('GOAL_TOOL_INVALID_UPDATE') const blockedWithoutReason = await execute(ctx, 'update_goal', { goal_id: created.id, revision: created.revision, action: 'blocked', }, root.agent) expect(blockedWithoutReason.error?.info?.code).toBe('GOAL_TOOL_INVALID_UPDATE') const blockedWithEmptyReason = await execute(ctx, 'update_goal', { goal_id: created.id, revision: created.revision, action: 'blocked', blocked_reason: ' ', }, root.agent) expect(blockedWithEmptyReason.error?.info?.code).toBe('GOAL_TOOL_INVALID_UPDATE') const completeWithReason = await execute(ctx, 'update_goal', { goal_id: created.id, revision: created.revision, action: 'complete', blocked_reason: 'Not a blocker.', }, root.agent) expect(completeWithReason.error?.info?.code).toBe('GOAL_TOOL_INVALID_UPDATE') const editWithReason = await execute(ctx, 'update_goal', { goal_id: created.id, revision: created.revision, action: 'edit', objective: 'still valid', blocked_reason: 'Not valid for edit.', }, root.agent) expect(editWithReason.error?.info?.code).toBe('GOAL_TOOL_INVALID_UPDATE') const malformedRef = await execute(ctx, 'update_goal', { goal_id: '', revision: 0, action: 'edit', objective: 'x', }, root.agent) expect(malformedRef.error?.info?.code).toBe('GOAL_TOOL_INVALID_UPDATE') }) it('accepts only empty fillers in fields unused by the selected action', async () => { const { ctx, root } = await harness() openTurn(root, { kind: 'user' }) let goal = ctx.goals.create(root.agent, { objective: 'valid' }) const edited = await execute(ctx, 'update_goal', { goal_id: goal.id, revision: goal.revision, action: 'edit', objective: 'edited', max_goal_rounds: 0, blocked_reason: '', }, root.agent) expect(resultGoal(edited)).toMatchObject({ objective: 'edited' }) goal = ctx.goals.get(root.agent)! const capped = await execute(ctx, 'update_goal', { goal_id: goal.id, revision: goal.revision, action: 'edit', objective: '', max_goal_rounds: 8, blocked_reason: '', }, root.agent) expect(resultGoal(capped)).toMatchObject({ objective: 'edited', maxGoalRounds: 8 }) goal = ctx.goals.get(root.agent)! const paused = await execute(ctx, 'update_goal', { goal_id: goal.id, revision: goal.revision, action: 'pause', objective: '', max_goal_rounds: 0, blocked_reason: '', }, root.agent) expect(resultGoal(paused)).toMatchObject({ phase: 'paused', objective: 'edited' }) goal = ctx.goals.get(root.agent)! const resumed = await execute(ctx, 'update_goal', { goal_id: goal.id, revision: goal.revision, action: 'resume', objective: '', max_goal_rounds: 0, blocked_reason: '', }, root.agent) expect(resultGoal(resumed)).toMatchObject({ phase: 'active', objective: 'edited' }) goal = ctx.goals.get(root.agent)! const blocked = await execute(ctx, 'update_goal', { goal_id: goal.id, revision: goal.revision, action: 'blocked', objective: '', max_goal_rounds: 0, blocked_reason: 'actual blocker', }, root.agent) expect(resultGoal(blocked)).toMatchObject({ phase: 'blocked' }) goal = ctx.goals.resume(root.agent, { id: goal.id, revision: goal.revision + 1 }) const complete = await execute(ctx, 'update_goal', { goal_id: goal.id, revision: goal.revision, action: 'complete', objective: '', max_goal_rounds: 0, blocked_reason: '', }, root.agent) expect(resultGoal(complete)).toMatchObject({ phase: 'complete', objective: 'edited' }) }) it('allows exact goal rounds to complete but not edit or pause', async () => { const { ctx, root } = await harness() const humanTurn = openTurn(root, { kind: 'user' }) const created = ctx.goals.create(root.agent, { objective: 'round-owned' }) closeTurn(root, humanTurn) openTurn(root, { kind: 'goal', goalId: created.id, revision: created.revision, round: 1 }) const edit = await execute(ctx, 'update_goal', { goal_id: created.id, revision: created.revision, action: 'edit', objective: 'forbidden', }, root.agent) expect(edit.error?.info?.code).toBe('GOAL_TOOL_AUTHORITY_REQUIRED') const complete = await execute(ctx, 'update_goal', { goal_id: created.id, revision: created.revision, action: 'complete', }, root.agent) expect(resultGoal(complete)).toMatchObject({ phase: 'complete', revision: 2, roundsStarted: 1 }) }) it('enforces the configured model self-block lower bound across admitted rounds', async () => { const { ctx, root } = await harness({ blockedAfterConsecutiveRounds: 3 }) let turn = openTurn(root, { kind: 'user' }) const created = ctx.goals.create(root.agent, { objective: 'blocked eventually' }) closeTurn(root, turn) const ref: GoalRef = { id: GoalId(created.id), revision: created.revision } for (let round = 1; round <= 2; round += 1) { turn = openTurn(root, { kind: 'goal', goalId: ref.id, revision: ref.revision, round }) const result = await execute(ctx, 'update_goal', { goal_id: ref.id, revision: ref.revision, action: 'blocked', blocked_reason: 'The required credential is still unavailable.', }, root.agent) expect(result.error?.info?.code).toBe('GOAL_TOOL_BLOCK_THRESHOLD') closeTurn(root, turn) } openTurn(root, { kind: 'goal', goalId: ref.id, revision: ref.revision, round: 3 }) const blocked = await execute(ctx, 'update_goal', { goal_id: ref.id, revision: ref.revision, action: 'blocked', blocked_reason: 'The required credential is still unavailable.', }, root.agent) expect(resultGoal(blocked)).toMatchObject({ phase: 'blocked', blockedReason: { code: 'model-reported', message: 'The required credential is still unavailable.' }, roundsStarted: 3, }) }) it('lets direct human authority block before the model threshold', async () => { const { ctx, root } = await harness({ blockedAfterConsecutiveRounds: 9 }) openTurn(root, { kind: 'user' }) const created = ctx.goals.create(root.agent, { objective: 'human stop' }) const blocked = await execute(ctx, 'update_goal', { goal_id: created.id, revision: created.revision, action: 'blocked', blocked_reason: 'The user asked to stop until a prerequisite is available.', }, root.agent) expect(resultGoal(blocked)).toMatchObject({ phase: 'blocked', blockedReason: { code: 'model-reported', message: 'The user asked to stop until a prerequisite is available.', }, roundsStarted: 0, }) }) })