Merge remote-tracking branch 'origin/master' into xtr/agent-loop-message-machine

# Conflicts:
#	docs/cordis-catalog/services.md
This commit is contained in:
_Kerman
2026-07-24 21:41:02 +08:00
41 changed files with 922 additions and 144 deletions
+2 -2
View File
@@ -6,9 +6,9 @@ The model-facing control surface for [`ctx.goals`](../goal/README.md): `get_goal
- `get_goal()` returns the current goal or `null`, including the compare-and-set id/revision, durable phase, admitted/capped goal rounds, any blocker reason, and current process-local activation.
- `create_goal(objective, max_goal_rounds?)` creates one goal from a direct top-level human turn. The model may infer long-running goal intent without an exact command phrase; non-human turns and subagents are rejected at execution.
- `update_goal(goal_id, revision, action, objective?, max_goal_rounds?, blocked_reason?)` supports `edit`, `pause`, `resume`, `complete`, and `blocked`. Replacements belong only to `edit`; `blocked_reason` is required only for `blocked` and is persisted with the stable code `model-reported`.
- `update_goal(goal_id, revision, action, objective?, max_goal_rounds?, blocked_reason?)` supports `edit`, `pause`, `resume`, `complete`, and `blocked`. Replacements belong only to `edit`; `blocked_reason` is required only for `blocked` and is persisted with the stable code `model-reported`. Strict-schema empty-string and zero fillers count as omitted, while meaningful values remain limited to their action.
All calls are exclusive, so a model-ordered batch observes earlier mutations and their new revisions. ACP and other clients receive pure generic cards: read for `get_goal`, other for mutations.
All calls are exclusive, so a model-ordered batch observes earlier mutations and their new revisions. ACP and other clients receive pure generic cards: read for `get_goal`, other for mutations. Mutation cards select the first meaningful action value and otherwise show the goal id, so accepted fillers never produce blank input.
All three canonical values match the compact JSON already rendered to Native callers: `{ goal: null }` or `{ goal: { id, revision, objective, phase, roundsStarted, maxGoalRounds, blockedReason? }, activation }`. Programmatic consumers therefore receive the same domain structure without parsing the rendered JSON.
+21 -7
View File
@@ -130,6 +130,16 @@ function resolveConfig(config: Config): ResolvedConfig {
return { blockedAfterConsecutiveRounds: blockedAfter }
}
/** Whether optional text is meaningful rather than a strict-schema empty filler. */
function hasText(value: string | undefined): value is string {
return value !== undefined && value !== ''
}
/** Whether an optional round cap is meaningful rather than a strict-schema zero filler. */
function hasRoundCap(value: number | undefined): value is number {
return value !== undefined && value !== 0
}
/** Build the exact compare-and-set ref from model arguments. */
function goalRef(goalId: string, revision: number): GoalRef {
if (goalId.length === 0 || goalId !== goalId.trim()
@@ -247,12 +257,12 @@ export function apply(ctx: Context, config: Config): void {
const execution = goalToolExecution(ctx, exec)
const ref = goalRef(args.goal_id, args.revision)
const replacements = {
...args.objective === undefined ? {} : { objective: args.objective },
...args.max_goal_rounds === undefined ? {} : { maxGoalRounds: args.max_goal_rounds },
...hasText(args.objective) ? { objective: args.objective } : {},
...hasRoundCap(args.max_goal_rounds) ? { maxGoalRounds: args.max_goal_rounds } : {},
}
if (args.action === 'edit') {
requireDirectHuman(ctx, execution)
if (args.blocked_reason !== undefined) {
if (hasText(args.blocked_reason)) {
throw new HarnessError('blocked_reason is valid only with action blocked', 'GOAL_TOOL_INVALID_UPDATE')
}
const goal = ctx.goals.edit(execution.agent, ref, replacements)
@@ -260,7 +270,7 @@ export function apply(ctx: Context, config: Config): void {
}
if (args.action === 'pause' || args.action === 'resume') {
requireDirectHuman(ctx, execution)
if (args.objective !== undefined || args.max_goal_rounds !== undefined || args.blocked_reason !== undefined) {
if (hasText(args.objective) || hasRoundCap(args.max_goal_rounds) || hasText(args.blocked_reason)) {
throw new HarnessError(
'objective and max_goal_rounds are valid only with action edit; blocked_reason is valid only with action blocked',
'GOAL_TOOL_INVALID_UPDATE',
@@ -272,13 +282,13 @@ export function apply(ctx: Context, config: Config): void {
return Promise.resolve(goalValue(goal))
}
const authority = completionAuthority(ctx, execution)
if (args.objective !== undefined || args.max_goal_rounds !== undefined) {
if (hasText(args.objective) || hasRoundCap(args.max_goal_rounds)) {
throw new HarnessError(
'objective and max_goal_rounds are valid only with action edit',
'GOAL_TOOL_INVALID_UPDATE',
)
}
if (args.action === 'complete' && args.blocked_reason !== undefined) {
if (args.action === 'complete' && hasText(args.blocked_reason)) {
throw new HarnessError('blocked_reason is valid only with action blocked', 'GOAL_TOOL_INVALID_UPDATE')
}
if (args.action === 'blocked'
@@ -305,7 +315,11 @@ export function apply(ctx: Context, config: Config): void {
presentCall: args => present(
`${args.action === 'blocked' ? 'Mark' : args.action.charAt(0).toUpperCase() + args.action.slice(1)} goal`,
'other',
args.blocked_reason ?? args.objective ?? args.goal_id,
hasText(args.blocked_reason)
? args.blocked_reason
: hasText(args.objective)
? args.objective
: hasRoundCap(args.max_goal_rounds) ? args.max_goal_rounds : args.goal_id,
),
}))
}
@@ -144,8 +144,17 @@ describe('goal tool registration and presentation', () => {
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()
})
@@ -425,6 +434,77 @@ describe('goal tool state transitions', () => {
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' })