mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
613 lines
26 KiB
TypeScript
613 lines
26 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { Context } from 'cordis'
|
|
import Loader from '@cordisjs/plugin-loader'
|
|
import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent'
|
|
import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent'
|
|
import GoalService, { GoalId } from '@deepseek-ai/dsh-goal'
|
|
import type { GoalRef } from '@deepseek-ai/dsh-goal'
|
|
import { createUserMessage, CallId } from '@deepseek-ai/dsh-llm'
|
|
import type { 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 enter the durable inbox. */
|
|
function stubAgent(rawId: string, supplied?: Session): StubAgent {
|
|
const session = supplied ?? Session.create(SessionId(rawId))
|
|
let status: AgentStatus = 'running'
|
|
const agent: Agent = {
|
|
id: session.id,
|
|
options: {},
|
|
session,
|
|
inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
|
|
get status() { return status },
|
|
ctx: new Context(),
|
|
send: () => {},
|
|
followup: () => {},
|
|
steer: () => ({ outcome: Promise.resolve({ status: 'rejected' as const }) }),
|
|
inject(input) {
|
|
this.inbox.append('next-step', input)
|
|
},
|
|
cancel() {},
|
|
runMaintenance: task => task(new AbortController().signal),
|
|
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
|
|
const message = createUserMessage({
|
|
content: [{ type: 'text', text }],
|
|
source,
|
|
})
|
|
stub.agent.inbox.append('next-turn', message)
|
|
const claimed = stub.agent.inbox.claim('next-turn', turn)
|
|
if (claimed.length === 0) throw new Error('expected queued turn input')
|
|
stub.session.append('turn/start', { turn })
|
|
for (const admitted of claimed) {
|
|
stub.session.append('user/message', admitted, { 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<ToolExecutionResult> {
|
|
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<string, unknown> {
|
|
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<string, unknown>
|
|
expect(result.value).toEqual(parsed)
|
|
return parsed
|
|
}
|
|
|
|
/** Read the returned goal sub-object. */
|
|
function resultGoal(result: ToolExecutionResult): Record<string, unknown> {
|
|
const goal = resultJson(result)['goal']
|
|
if (typeof goal !== 'object' || goal === null) throw new Error('expected returned goal')
|
|
return goal as Record<string, unknown>
|
|
}
|
|
|
|
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 = Session.create(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)
|
|
openTurn(root, {
|
|
kind: 'goal', goalId: created.id, revision: created.revision, round: 1,
|
|
})
|
|
root.session.append('user/message', createUserMessage({
|
|
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 })
|
|
})
|
|
|
|
it('injects one wrap-up instruction for 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(paused.concludesTurn).toBeUndefined()
|
|
expect(paused.additionalContexts).toBeUndefined()
|
|
const resumed = resultGoal(await execute(ctx, 'update_goal', {
|
|
goal_id: created.id, revision: 2, action: 'resume',
|
|
}, root.agent))
|
|
closeTurn(root, humanTurn)
|
|
|
|
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(complete.concludesTurn).toBeUndefined()
|
|
const contexts = complete.additionalContexts ?? []
|
|
expect(contexts).toHaveLength(1)
|
|
expect(contexts[0]?.source).toEqual({
|
|
kind: 'plugin',
|
|
plugin: 'tool-goal',
|
|
form: 'notice',
|
|
summary: 'complete: pause cleanly',
|
|
})
|
|
const block = contexts[0]?.content[0]
|
|
if (block?.type !== 'text') throw new Error('expected one text wrap-up block')
|
|
expect(block.text).toContain('<goal_complete>')
|
|
expect(block.text).toContain('"pause cleanly"')
|
|
expect(block.text).toContain("Do not call any more tools in this run; further work waits for the user's next instruction.")
|
|
})
|
|
|
|
it('completes without a wrap-up instruction under direct human authority', async () => {
|
|
const { ctx, root } = await harness()
|
|
openTurn(root, { kind: 'user' })
|
|
const created = ctx.goals.create(root.agent, { objective: 'finish now' })
|
|
const complete = await execute(ctx, 'update_goal', {
|
|
goal_id: created.id, revision: created.revision, action: 'complete',
|
|
}, root.agent)
|
|
expect(resultGoal(complete)).toMatchObject({ phase: 'complete' })
|
|
expect(complete.concludesTurn).toBeUndefined()
|
|
expect(complete.additionalContexts).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', { source: '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,
|
|
})
|
|
expect(blocked.concludesTurn).toBeUndefined()
|
|
const contexts = blocked.additionalContexts ?? []
|
|
expect(contexts).toHaveLength(1)
|
|
const block = contexts[0]?.content[0]
|
|
if (block?.type !== 'text') throw new Error('expected one text wrap-up block')
|
|
expect(block.text).toContain('<goal_blocked>')
|
|
expect(block.text).toContain('The required credential is still unavailable.')
|
|
expect(block.text).toContain("Do not call any more tools in this run; further work waits for the user's next instruction.")
|
|
})
|
|
|
|
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,
|
|
})
|
|
expect(blocked.concludesTurn).toBeUndefined()
|
|
expect(blocked.additionalContexts).toBeUndefined()
|
|
})
|
|
})
|