@namzu/sdk 12.0.1 → 12.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -0
- package/dist/agents/AbstractAgent.d.ts +20 -0
- package/dist/agents/AbstractAgent.d.ts.map +1 -1
- package/dist/agents/AbstractAgent.js +32 -0
- package/dist/agents/AbstractAgent.js.map +1 -1
- package/dist/agents/lock.d.ts.map +1 -1
- package/dist/agents/lock.js +8 -1
- package/dist/agents/lock.js.map +1 -1
- package/dist/manager/agent/__tests__/a-fan-out-to-one-agent-id.test.d.ts +2 -0
- package/dist/manager/agent/__tests__/a-fan-out-to-one-agent-id.test.d.ts.map +1 -0
- package/dist/manager/agent/__tests__/a-fan-out-to-one-agent-id.test.js +101 -0
- package/dist/manager/agent/__tests__/a-fan-out-to-one-agent-id.test.js.map +1 -0
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +21 -1
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/runtime/query/__tests__/the-other-approval-surface-names-the-agent.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/the-other-approval-surface-names-the-agent.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/the-other-approval-surface-names-the-agent.test.js +81 -0
- package/dist/runtime/query/__tests__/the-other-approval-surface-names-the-agent.test.js.map +1 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +1 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/plan.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/plan.js +1 -0
- package/dist/runtime/query/iteration/phases/plan.js.map +1 -1
- package/dist/tools/coordinator/__tests__/an-approver-is-never-shown-an-impossible-step.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/an-approver-is-never-shown-an-impossible-step.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/an-approver-is-never-shown-an-impossible-step.test.js +115 -0
- package/dist/tools/coordinator/__tests__/an-approver-is-never-shown-an-impossible-step.test.js.map +1 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +36 -0
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/types/agent/core.d.ts +21 -0
- package/dist/types/agent/core.d.ts.map +1 -1
- package/dist/types/agent/factory.d.ts +18 -0
- package/dist/types/agent/factory.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +16 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/AbstractAgent.ts +35 -0
- package/src/agents/lock.ts +10 -1
- package/src/manager/agent/__tests__/a-fan-out-to-one-agent-id.test.ts +118 -0
- package/src/manager/agent/lifecycle.ts +21 -1
- package/src/runtime/query/__tests__/the-other-approval-surface-names-the-agent.test.ts +95 -0
- package/src/runtime/query/index.ts +1 -0
- package/src/runtime/query/iteration/phases/plan.ts +1 -0
- package/src/tools/coordinator/__tests__/an-approver-is-never-shown-an-impossible-step.test.ts +153 -0
- package/src/tools/coordinator/index.ts +40 -0
- package/src/types/agent/core.ts +22 -0
- package/src/types/agent/factory.ts +20 -0
- package/src/types/hitl/index.ts +18 -0
|
@@ -122,6 +122,22 @@ export interface PlanApprovalData {
|
|
|
122
122
|
id: string;
|
|
123
123
|
description: string;
|
|
124
124
|
toolName?: string;
|
|
125
|
+
/**
|
|
126
|
+
* Which agent the step is to be delegated to, when it is delegated.
|
|
127
|
+
*
|
|
128
|
+
* `PlanStep` gained this so an approver could see WHICH agent a step
|
|
129
|
+
* goes to rather than only THAT it delegates — approving "delegate
|
|
130
|
+
* this" is not approving "delegate this to the agent with shell
|
|
131
|
+
* access". It reached `PlanApprovalRequest`, which is the shape a host
|
|
132
|
+
* sees when it installs its own handler on `PlanManager`.
|
|
133
|
+
*
|
|
134
|
+
* It did not reach here, and this is the ordinary path: every host
|
|
135
|
+
* using `resumeHandler` is served by this type, and both mappers that
|
|
136
|
+
* build it copy field by field. So the fix landed on one of the two
|
|
137
|
+
* approval surfaces and the busier one kept showing
|
|
138
|
+
* `toolName: 'create_task'` and nothing else.
|
|
139
|
+
*/
|
|
140
|
+
agentId?: string;
|
|
125
141
|
dependsOn: string[];
|
|
126
142
|
order: number;
|
|
127
143
|
}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/hitl/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/hitl/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/types/hitl/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/types/hitl/index.ts"],"names":[],"mappings":"AA4PA,MAAM,UAAU,kBAAkB,CAAC,OAA4B;IAC9D,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;QACtB,KAAK,eAAe;YACnB,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAA;QACnD,KAAK,aAAa;YACjB,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC,CAAA;QACpD,KAAK,sBAAsB;YAC1B,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAA;QAC/C,KAAK,eAAe;YACnB,2DAA2D;YAC3D,yDAAyD;YACzD,6DAA6D;YAC7D,uCAAuC;YACvC,OAAO,OAAO,CAAC,OAAO,CAAC;gBACtB,MAAM,EAAE,iBAAiB;gBACzB,iBAAiB,EAAE,EAAE;gBACrB,QAAQ,EAAE,mEAAmE;gBAC7E,UAAU,EAAE,OAAO,CAAC,QAAQ,CAAC,UAAU;aACvC,CAAC,CAAA;QACH,OAAO,CAAC,CAAC,CAAC;YACT,MAAM,WAAW,GAAU,OAAO,CAAA;YAClC,MAAM,IAAI,KAAK,CAAC,gCAAiC,WAAmC,CAAC,IAAI,EAAE,CAAC,CAAA;QAC7F,CAAC;IACF,CAAC;AACF,CAAC"}
|
package/package.json
CHANGED
|
@@ -55,6 +55,41 @@ export abstract class AbstractAgent<
|
|
|
55
55
|
|
|
56
56
|
abstract run(input: AgentInput, config: TConfig, listener?: RunEventListener): Promise<TResult>
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* A fresh shell of this agent, for a run that must not share one.
|
|
60
|
+
*
|
|
61
|
+
* See {@link Agent.forRun}. An agent is a shell around metadata — every
|
|
62
|
+
* per-run decision arrives in `config` and `input` — so a second instance
|
|
63
|
+
* costs one object and gives the run its own abort controller and run id,
|
|
64
|
+
* which is precisely what the invocation lock is protecting.
|
|
65
|
+
*
|
|
66
|
+
* Rebuilt from `this.constructor` and `this.metadata`, which covers every
|
|
67
|
+
* agent in this package: they all take metadata and nothing else. A
|
|
68
|
+
* subclass with a different constructor signature will throw here, and the
|
|
69
|
+
* answer to that is `this` — the caller then shares the shell and gets the
|
|
70
|
+
* existing refusal on a concurrent run, which is the behaviour before this
|
|
71
|
+
* existed. Losing parallelism is a worse outcome than not having it; losing
|
|
72
|
+
* the run is not on the table.
|
|
73
|
+
*
|
|
74
|
+
* A host whose agent needs real construction arguments supplies
|
|
75
|
+
* `AgentDefinition.createAgent` instead, which wins over this.
|
|
76
|
+
*/
|
|
77
|
+
forRun(): this {
|
|
78
|
+
try {
|
|
79
|
+
const Ctor = this.constructor as unknown as new (metadata: AgentMetadata) => this
|
|
80
|
+
return new Ctor(this.metadata)
|
|
81
|
+
} catch (err) {
|
|
82
|
+
this.log.warn(
|
|
83
|
+
'Could not build a per-run shell; concurrent runs of this agent will still be refused',
|
|
84
|
+
{
|
|
85
|
+
agentId: this.metadata.id,
|
|
86
|
+
error: err instanceof Error ? err.message : String(err),
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
return this
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
58
93
|
/**
|
|
59
94
|
* Acquire the invocation lock to prevent concurrent execution.
|
|
60
95
|
* Returns a Disposable that must be disposed to release the lock.
|
package/src/agents/lock.ts
CHANGED
|
@@ -11,7 +11,16 @@ export class ConcurrentInvocationError extends Error {
|
|
|
11
11
|
readonly agentId: string
|
|
12
12
|
|
|
13
13
|
constructor(agentId: string) {
|
|
14
|
-
|
|
14
|
+
// Names the remedy, because the refusal alone sent readers looking for a
|
|
15
|
+
// concurrency bug in their own code. An agent instance holds per-run
|
|
16
|
+
// state — an abort controller and the run id — so two overlapping runs
|
|
17
|
+
// on one shell would cancel each other; the answer is a second shell,
|
|
18
|
+
// not a second attempt. Delegated spawns get one automatically via
|
|
19
|
+
// `Agent.forRun`, so reaching this from a fan-out means the agent
|
|
20
|
+
// could not be rebuilt and wants `AgentDefinition.createAgent`.
|
|
21
|
+
super(
|
|
22
|
+
`Agent ${agentId} is already processing. Concurrent invocations of one instance are not allowed, because its abort controller and run id are instance state and two runs would cancel each other. Run a second instance instead — or, for a delegated spawn, give its AgentDefinition a \`createAgent\` factory so each child gets its own.`,
|
|
23
|
+
)
|
|
15
24
|
this.name = 'ConcurrentInvocationError'
|
|
16
25
|
this.agentId = agentId
|
|
17
26
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { ReactiveAgent } from '../../../agents/ReactiveAgent.js'
|
|
4
|
+
import { ToolRegistry } from '../../../registry/index.js'
|
|
5
|
+
import type { AgentDefinition } from '../../../types/agent/factory.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A fan-out naming the same agent four times ran one child and lost three.
|
|
9
|
+
*
|
|
10
|
+
* `AgentRegistry` hands out ONE `typedAgent` per registered id, and an instance
|
|
11
|
+
* refuses a second concurrent `run` — correctly, because its abort controller
|
|
12
|
+
* and run id are instance state and two overlapping runs would cancel each
|
|
13
|
+
* other. So four `create_task` calls at one specialist produced one result and
|
|
14
|
+
* three `ConcurrentInvocationError`s.
|
|
15
|
+
*
|
|
16
|
+
* The prescribed remedy already existed in the docs — "a host that wants
|
|
17
|
+
* parallelism constructs a second instance" — and was unreachable from
|
|
18
|
+
* delegation, where the definition owns the instance and the caller has only an
|
|
19
|
+
* id. Observed live on published 12.0.1: four launches, three lost.
|
|
20
|
+
*
|
|
21
|
+
* These cover the shell itself. That the manager USES it per spawn is covered
|
|
22
|
+
* where the manager is driven; what has to hold here is that asking for a
|
|
23
|
+
* per-run shell gives you a genuinely separate one.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const metadata = {
|
|
27
|
+
id: 'worker',
|
|
28
|
+
name: 'worker',
|
|
29
|
+
version: '1.0.0',
|
|
30
|
+
category: 'general',
|
|
31
|
+
description: 'a worker',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('an agent can hand out a shell a single run has to itself', () => {
|
|
35
|
+
it('returns a different instance', () => {
|
|
36
|
+
const agent = new ReactiveAgent(metadata)
|
|
37
|
+
|
|
38
|
+
expect(agent.forRun()).not.toBe(agent)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('keeps the identity, because it is the same agent', () => {
|
|
42
|
+
const agent = new ReactiveAgent(metadata)
|
|
43
|
+
const shell = agent.forRun()
|
|
44
|
+
|
|
45
|
+
expect(shell.metadata.id).toBe('worker')
|
|
46
|
+
expect(shell.type).toBe(agent.type)
|
|
47
|
+
expect(shell.getCapabilities()).toEqual(agent.getCapabilities())
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('gives each shell its own invocation lock, which is the whole point', async () => {
|
|
51
|
+
// Locking one must not lock the other. Asserted through the public
|
|
52
|
+
// surface: a run that never settles holds the lock, and a second run on
|
|
53
|
+
// a SEPARATE shell must still be admitted.
|
|
54
|
+
// ONE registered agent, two shells — the registry's shape, and the
|
|
55
|
+
// shape the fan-out actually hits.
|
|
56
|
+
const registered = new ReactiveAgent(metadata)
|
|
57
|
+
const first = registered.forRun()
|
|
58
|
+
const second = registered.forRun()
|
|
59
|
+
|
|
60
|
+
// A provider that starts and never finishes, so each run holds its
|
|
61
|
+
// shell's lock for the duration of the assertion.
|
|
62
|
+
const provider = {
|
|
63
|
+
// biome-ignore lint/correctness/useYield: it never produces anything, on purpose
|
|
64
|
+
async *chatStream() {
|
|
65
|
+
await new Promise<never>(() => {})
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
const config = {
|
|
69
|
+
model: 'mock',
|
|
70
|
+
tokenBudget: 10_000,
|
|
71
|
+
timeoutMs: 10_000,
|
|
72
|
+
maxIterations: 2,
|
|
73
|
+
provider,
|
|
74
|
+
tools: new ToolRegistry(),
|
|
75
|
+
systemPrompt: 'hold',
|
|
76
|
+
sessionId: 'ses_fan' as never,
|
|
77
|
+
threadId: 'thd_fan' as never,
|
|
78
|
+
projectId: 'prj_fan' as never,
|
|
79
|
+
tenantId: 'tnt_fan' as never,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Start one run on each shell; neither resolves, and neither should
|
|
83
|
+
// refuse. A shared shell would reject the second synchronously.
|
|
84
|
+
const a = first.run({ messages: [], workingDirectory: '/tmp' } as never, config as never)
|
|
85
|
+
const b = second.run({ messages: [], workingDirectory: '/tmp' } as never, config as never)
|
|
86
|
+
|
|
87
|
+
await expect(
|
|
88
|
+
Promise.race([
|
|
89
|
+
Promise.all([a, b]).then(() => 'settled'),
|
|
90
|
+
new Promise((r) => setTimeout(() => r('still running'), 50)),
|
|
91
|
+
]),
|
|
92
|
+
).resolves.toBe('still running')
|
|
93
|
+
|
|
94
|
+
void a.catch(() => {})
|
|
95
|
+
void b.catch(() => {})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('a definition may override the shell with its own factory', () => {
|
|
99
|
+
// The escape hatch for an agent that needs real construction
|
|
100
|
+
// arguments, which `forRun`'s metadata-only rebuild cannot supply.
|
|
101
|
+
let built = 0
|
|
102
|
+
const definition: AgentDefinition = {
|
|
103
|
+
info: { ...metadata, tools: [], defaults: {} } as never,
|
|
104
|
+
typedAgent: new ReactiveAgent(metadata) as never,
|
|
105
|
+
createAgent: () => {
|
|
106
|
+
built += 1
|
|
107
|
+
return new ReactiveAgent(metadata) as never
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const one = definition.createAgent?.()
|
|
112
|
+
const two = definition.createAgent?.()
|
|
113
|
+
|
|
114
|
+
expect(built).toBe(2)
|
|
115
|
+
expect(one).not.toBe(two)
|
|
116
|
+
expect(one).not.toBe(definition.typedAgent)
|
|
117
|
+
})
|
|
118
|
+
})
|
|
@@ -123,7 +123,27 @@ export class AgentManager {
|
|
|
123
123
|
)
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
// A shell this task has to itself, not the registry's shared instance.
|
|
127
|
+
//
|
|
128
|
+
// `resolve` returns one `typedAgent` per registered id, and an instance
|
|
129
|
+
// refuses a second concurrent `run` because it holds per-run state. So
|
|
130
|
+
// a fan-out naming the same `agent_id` four times drove four runs at one
|
|
131
|
+
// shell: one worked and three died with `ConcurrentInvocationError` —
|
|
132
|
+
// while `create_task`'s own description tells the model that this
|
|
133
|
+
// fan-out is the thing to do. Observed live on 12.0.1, four launches,
|
|
134
|
+
// three lost.
|
|
135
|
+
//
|
|
136
|
+
// The remedy was already written down — "a host that wants parallelism
|
|
137
|
+
// constructs a second instance" — and was unreachable here, because the
|
|
138
|
+
// definition owns the instance and this path only has an id.
|
|
139
|
+
//
|
|
140
|
+
// Nothing else about the child is shared: its abort signal is the task's
|
|
141
|
+
// own (`input.signal` below), its config is rebuilt per spawn by
|
|
142
|
+
// `configBuilder`, and the manager cancels through the task rather than
|
|
143
|
+
// the agent. The shell was the only shared thing left.
|
|
144
|
+
const definitionForSpawn = this.registry.getOrThrow(options.agentId)
|
|
145
|
+
const sharedAgent = definitionForSpawn.typedAgent
|
|
146
|
+
const agent = definitionForSpawn.createAgent?.() ?? sharedAgent.forRun?.() ?? sharedAgent
|
|
127
147
|
|
|
128
148
|
const childAbortController = createChildAbortController(context.parentAbortController)
|
|
129
149
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import type { PlanManager } from '../../../manager/plan/lifecycle.js'
|
|
4
|
+
import { MockLLMProvider, registerMock } from '../../../provider/index.js'
|
|
5
|
+
import { ToolRegistry } from '../../../registry/index.js'
|
|
6
|
+
import type { HITLResumeDecision, PlanApprovalData } from '../../../types/hitl/index.js'
|
|
7
|
+
import {
|
|
8
|
+
generateProjectId,
|
|
9
|
+
generateSessionId,
|
|
10
|
+
generateTenantId,
|
|
11
|
+
generateThreadId,
|
|
12
|
+
} from '../../../utils/id.js'
|
|
13
|
+
import { drainQuery } from '../index.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* There are TWO approval surfaces, and the fix landed on one of them.
|
|
17
|
+
*
|
|
18
|
+
* `PlanStep.agentId` was added so an approver could see WHICH agent a step goes
|
|
19
|
+
* to rather than only THAT it delegates. It reached `PlanApprovalRequest` — the
|
|
20
|
+
* shape a host sees when it installs its own handler on `PlanManager` — and the
|
|
21
|
+
* test written at the time asserted on exactly that, by constructing a
|
|
22
|
+
* `PlanManager` directly.
|
|
23
|
+
*
|
|
24
|
+
* It did not reach `PlanApprovalData`, which is what every `resumeHandler` host
|
|
25
|
+
* receives, because that type declared its own step shape and both mappers copy
|
|
26
|
+
* field by field. So the busier surface kept showing `toolName: 'create_task'`
|
|
27
|
+
* and nothing else — the precise behaviour the change was supposed to end.
|
|
28
|
+
*
|
|
29
|
+
* A live run did not catch it either: the run observes `plan_ready`, which
|
|
30
|
+
* carries whole `PlanStep`s and therefore always had `agentId`. Watching the
|
|
31
|
+
* event stream confirmed the field existed somewhere, which is not the same
|
|
32
|
+
* question as whether the approver gets it.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
registerMock()
|
|
36
|
+
|
|
37
|
+
async function agentIdSeenByResumeHandler(): Promise<PlanApprovalData['steps']> {
|
|
38
|
+
let seen: PlanApprovalData['steps'] | undefined
|
|
39
|
+
|
|
40
|
+
await drainQuery({
|
|
41
|
+
provider: new MockLLMProvider({ responses: [{ content: 'done' }] } as never),
|
|
42
|
+
tools: new ToolRegistry(),
|
|
43
|
+
agentId: 'a',
|
|
44
|
+
agentName: 'A',
|
|
45
|
+
messages: [{ role: 'user', content: 'go' }],
|
|
46
|
+
workingDirectory: process.cwd(),
|
|
47
|
+
runConfig: { model: 'mock', tokenBudget: 100_000, timeoutMs: 30_000, maxIterations: 4 },
|
|
48
|
+
projectId: generateProjectId(),
|
|
49
|
+
sessionId: generateSessionId(),
|
|
50
|
+
threadId: generateThreadId(),
|
|
51
|
+
tenantId: generateTenantId(),
|
|
52
|
+
// The ordinary host path: a resumeHandler, not a hand-installed
|
|
53
|
+
// PlanManager handler.
|
|
54
|
+
resumeHandler: async (request: { type: string; plan?: PlanApprovalData }) => {
|
|
55
|
+
if (request.type === 'plan_approval' && request.plan) seen = request.plan.steps
|
|
56
|
+
return { action: 'approve_plan' } as HITLResumeDecision
|
|
57
|
+
},
|
|
58
|
+
onContextCreated: ({ planManager }: { planManager: PlanManager }) => {
|
|
59
|
+
planManager.startGenerating('the work')
|
|
60
|
+
planManager.addStep({
|
|
61
|
+
id: 'step_1',
|
|
62
|
+
description: 'delegated work',
|
|
63
|
+
agentId: 'shell-runner',
|
|
64
|
+
toolName: 'create_task',
|
|
65
|
+
dependsOn: [],
|
|
66
|
+
order: 1,
|
|
67
|
+
})
|
|
68
|
+
planManager.addStep({ id: 'step_2', description: 'my own work', dependsOn: [], order: 2 })
|
|
69
|
+
planManager.markReady()
|
|
70
|
+
void planManager.requestApproval()
|
|
71
|
+
},
|
|
72
|
+
} as never)
|
|
73
|
+
|
|
74
|
+
if (!seen) throw new Error('the resume handler never received a plan approval')
|
|
75
|
+
return seen
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
describe('the resumeHandler approval surface names the agent too', () => {
|
|
79
|
+
it('carries agentId through to the host', async () => {
|
|
80
|
+
const steps = await agentIdSeenByResumeHandler()
|
|
81
|
+
|
|
82
|
+
expect(steps.map((s) => s.agentId)).toEqual(['shell-runner', undefined])
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('still distinguishes the two steps by more than toolName', async () => {
|
|
86
|
+
// The defect in the shape that matters: without agentId both delegated
|
|
87
|
+
// steps reach the approver identical, and an orchestrator-owned step is
|
|
88
|
+
// told apart only by an absent toolName.
|
|
89
|
+
const steps = await agentIdSeenByResumeHandler()
|
|
90
|
+
const delegated = steps.find((s) => s.id === 'step_1')
|
|
91
|
+
|
|
92
|
+
expect(delegated?.toolName).toBe('create_task')
|
|
93
|
+
expect(delegated?.agentId).toBe('shell-runner')
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { PlanManager } from '../../../manager/plan/lifecycle.js'
|
|
4
|
+
import type { TaskGateway } from '../../../types/agent/gateway.js'
|
|
5
|
+
import type { RunId } from '../../../types/ids/index.js'
|
|
6
|
+
import type { PlanApprovalRequest } from '../../../types/plan/index.js'
|
|
7
|
+
import type { ToolContext } from '../../../types/tool/index.js'
|
|
8
|
+
import { buildCoordinatorTools } from '../index.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A plan could name an agent the launch would then refuse.
|
|
12
|
+
*
|
|
13
|
+
* `create_task` constrains `agent_id` with a closed enum; `approve_plan` typed
|
|
14
|
+
* the same field as a bare string. So a model could propose, and a human could
|
|
15
|
+
* approve, "delegate to X" for an X that `create_task` rejects at schema-parse
|
|
16
|
+
* time — the run then burns a turn on a step a human had already blessed.
|
|
17
|
+
*
|
|
18
|
+
* The check lives in `execute`, not in the schema, and that is deliberate.
|
|
19
|
+
* `approve_plan` is mounted even with an EMPTY roster, because planning with no
|
|
20
|
+
* delegates and a human channel is a supported configuration — and `z.enum([])`
|
|
21
|
+
* renders as `{"not":{}}`, the shape `delegateSchema` already refuses because a
|
|
22
|
+
* strict tool-schema validator rejects the whole request over it rather than
|
|
23
|
+
* the one tool. `create_task` escapes that by being withheld entirely; this
|
|
24
|
+
* tool cannot be. Enforcing in `execute` as well is the precedent the canonical
|
|
25
|
+
* `Agent` tool set for complete mediation.
|
|
26
|
+
*
|
|
27
|
+
* It runs BEFORE `startGenerating`, so the refusal costs no half-built plan and
|
|
28
|
+
* the human is never shown the bad step at all.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const RUN = 'run_roster' as RunId
|
|
32
|
+
|
|
33
|
+
function unusedGateway(): TaskGateway {
|
|
34
|
+
return {
|
|
35
|
+
async createTask() {
|
|
36
|
+
throw new Error('this test never launches')
|
|
37
|
+
},
|
|
38
|
+
async waitForTask() {
|
|
39
|
+
throw new Error('this test never waits')
|
|
40
|
+
},
|
|
41
|
+
async continueTask() {},
|
|
42
|
+
cancelTask() {},
|
|
43
|
+
getTask() {
|
|
44
|
+
return undefined
|
|
45
|
+
},
|
|
46
|
+
listTasks() {
|
|
47
|
+
return []
|
|
48
|
+
},
|
|
49
|
+
onTaskCompleted() {
|
|
50
|
+
return () => {}
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ctx(): ToolContext {
|
|
56
|
+
return {
|
|
57
|
+
runId: RUN,
|
|
58
|
+
workingDirectory: '/tmp/test',
|
|
59
|
+
abortSignal: new AbortController().signal,
|
|
60
|
+
env: {},
|
|
61
|
+
log: () => {},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Run `approve_plan`, reporting both its result and what the approver saw. */
|
|
66
|
+
async function approve(
|
|
67
|
+
roster: string[],
|
|
68
|
+
steps: Array<{ description: string; agent_id?: string }>,
|
|
69
|
+
): Promise<{ result: Awaited<ReturnType<ReturnType<typeof build>>>; seen?: PlanApprovalRequest }> {
|
|
70
|
+
let seen: PlanApprovalRequest | undefined
|
|
71
|
+
const pm = new PlanManager(RUN, async (request) => {
|
|
72
|
+
seen = request
|
|
73
|
+
return { approved: true }
|
|
74
|
+
})
|
|
75
|
+
const run = build(roster, pm)
|
|
76
|
+
const result = await run(steps)
|
|
77
|
+
return { result, ...(seen ? { seen } : {}) }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function build(roster: string[], pm: PlanManager) {
|
|
81
|
+
const tools = buildCoordinatorTools({
|
|
82
|
+
gateway: unusedGateway(),
|
|
83
|
+
workingDirectory: '/tmp/test',
|
|
84
|
+
allowedAgentIds: roster,
|
|
85
|
+
getPlanManager: () => pm,
|
|
86
|
+
})
|
|
87
|
+
const approvePlan = tools.find((t) => t.name === 'approve_plan')
|
|
88
|
+
if (!approvePlan) throw new Error('approve_plan missing from coordinator builder')
|
|
89
|
+
return (steps: Array<{ description: string; agent_id?: string }>) =>
|
|
90
|
+
approvePlan.execute({ title: 'do it', summary: 'a plan', steps }, ctx())
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
describe('a plan may only name an agent the run can actually launch', () => {
|
|
94
|
+
it('refuses a step naming an agent outside the roster', async () => {
|
|
95
|
+
const { result } = await approve(
|
|
96
|
+
['researcher', 'writer'],
|
|
97
|
+
[{ description: 'audit the deps', agent_id: 'security-auditor' }],
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
expect(result.success).toBe(false)
|
|
101
|
+
expect(result.error).toContain('security-auditor')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('names the roster, so the model can correct itself in one turn', async () => {
|
|
105
|
+
const { result } = await approve(
|
|
106
|
+
['researcher', 'writer'],
|
|
107
|
+
[{ description: 'audit', agent_id: 'nobody' }],
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
expect(result.error).toContain('researcher')
|
|
111
|
+
expect(result.error).toContain('writer')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('never shows the approver the step it refused', async () => {
|
|
115
|
+
// The whole point of checking before `startGenerating`: a human must
|
|
116
|
+
// not be asked to approve work that cannot run.
|
|
117
|
+
const { seen } = await approve(
|
|
118
|
+
['researcher'],
|
|
119
|
+
[{ description: 'audit', agent_id: 'security-auditor' }],
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
expect(seen).toBeUndefined()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('leaves no half-built plan behind', async () => {
|
|
126
|
+
const pm = new PlanManager(RUN, async () => ({ approved: true }))
|
|
127
|
+
const run = build(['researcher'], pm)
|
|
128
|
+
|
|
129
|
+
await run([{ description: 'audit', agent_id: 'ghost' }])
|
|
130
|
+
|
|
131
|
+
expect(pm.active).toBeNull()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('says something different when the run has no delegates at all', async () => {
|
|
135
|
+
// `approve_plan` is mounted on an empty roster on purpose — planning
|
|
136
|
+
// without delegation is supported — so the message has to explain that
|
|
137
|
+
// rather than list an empty set.
|
|
138
|
+
const { result } = await approve([], [{ description: 'audit', agent_id: 'anyone' }])
|
|
139
|
+
|
|
140
|
+
expect(result.success).toBe(false)
|
|
141
|
+
expect(result.error).toContain('no delegates')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('still admits a plan that delegates only within the roster', async () => {
|
|
145
|
+
const { result, seen } = await approve(
|
|
146
|
+
['researcher', 'writer'],
|
|
147
|
+
[{ description: 'gather', agent_id: 'researcher' }, { description: 'summarize' }],
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
expect(result.success).toBe(true)
|
|
151
|
+
expect(seen?.steps.map((s) => s.agentId)).toEqual(['researcher', undefined])
|
|
152
|
+
})
|
|
153
|
+
})
|
|
@@ -1015,6 +1015,46 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
1015
1015
|
return { success: false, output: '', error: dependencies.error }
|
|
1016
1016
|
}
|
|
1017
1017
|
|
|
1018
|
+
// The roster, checked here rather than in the schema, and BEFORE
|
|
1019
|
+
// the plan is built — so a human is never shown a step naming an
|
|
1020
|
+
// agent that cannot run it.
|
|
1021
|
+
//
|
|
1022
|
+
// `create_task` constrains the same field with a closed enum, so
|
|
1023
|
+
// a plan could name an agent the launch would then refuse. The
|
|
1024
|
+
// mismatch used to be invisible because the name was dropped on
|
|
1025
|
+
// the way to the approver; now that a step carries it, an
|
|
1026
|
+
// approver could read "delegate to X" for an X that does not
|
|
1027
|
+
// exist.
|
|
1028
|
+
//
|
|
1029
|
+
// NOT closed in the schema, deliberately. `approve_plan` is
|
|
1030
|
+
// mounted even with an empty roster — planning with no delegates
|
|
1031
|
+
// and a human channel is a supported configuration — and
|
|
1032
|
+
// `z.enum([])` renders as `{"not":{}}`, the shape `delegateSchema`
|
|
1033
|
+
// already refuses because a strict tool-schema validator rejects
|
|
1034
|
+
// the whole request over it rather than the one tool.
|
|
1035
|
+
// `create_task` escapes that by being withheld entirely; this
|
|
1036
|
+
// tool cannot be.
|
|
1037
|
+
//
|
|
1038
|
+
// Enforcing in `execute` as well as the schema is the precedent
|
|
1039
|
+
// the canonical `Agent` tool set for complete mediation.
|
|
1040
|
+
const unknownAgents = [
|
|
1041
|
+
...new Set(
|
|
1042
|
+
steps
|
|
1043
|
+
.map((s) => s.agent_id)
|
|
1044
|
+
.filter((id): id is string => Boolean(id) && !agentIds.includes(id as string)),
|
|
1045
|
+
),
|
|
1046
|
+
]
|
|
1047
|
+
if (unknownAgents.length > 0) {
|
|
1048
|
+
return {
|
|
1049
|
+
success: false,
|
|
1050
|
+
output: '',
|
|
1051
|
+
error:
|
|
1052
|
+
agentIds.length === 0
|
|
1053
|
+
? `This plan delegates to ${unknownAgents.join(', ')}, but this run has no delegates. Plan the work as your own steps and omit agent_id.`
|
|
1054
|
+
: `No such agent: ${unknownAgents.join(', ')}. Delegate only to ${agentIds.join(', ')}, or omit agent_id for a step you carry out yourself.`,
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1018
1058
|
pm.startGenerating(title)
|
|
1019
1059
|
for (let i = 0; i < steps.length; i++) {
|
|
1020
1060
|
const step = steps[i]
|
package/src/types/agent/core.ts
CHANGED
|
@@ -17,6 +17,28 @@ export interface Agent<
|
|
|
17
17
|
|
|
18
18
|
run(input: AgentInput, config: TConfig, listener?: RunEventListener): Promise<TResult>
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* A shell of this agent that one run may have to itself.
|
|
22
|
+
*
|
|
23
|
+
* An agent instance holds per-run state — an abort controller, the id of
|
|
24
|
+
* the run in flight — and refuses a second concurrent `run` because of it.
|
|
25
|
+
* That refusal is correct for a host calling `run` twice on purpose: two
|
|
26
|
+
* overlapping runs would share one abort controller, so cancelling either
|
|
27
|
+
* kills both.
|
|
28
|
+
*
|
|
29
|
+
* It is wrong for delegation, which is why this exists. `AgentRegistry`
|
|
30
|
+
* hands out ONE instance per registered id, so a fan-out naming the same
|
|
31
|
+
* `agent_id` four times drove four runs at one shell: one worked and three
|
|
32
|
+
* died with `ConcurrentInvocationError`. The prescribed remedy — "construct
|
|
33
|
+
* a second instance" — was unreachable from there, because the definition
|
|
34
|
+
* owns the instance and the caller only has an id.
|
|
35
|
+
*
|
|
36
|
+
* OPTIONAL, and absence is safe: a manager that cannot get a fresh shell
|
|
37
|
+
* falls back to the shared one and the refusal stands, which is loud rather
|
|
38
|
+
* than wrong. `AbstractAgent` implements it for every agent built on it.
|
|
39
|
+
*/
|
|
40
|
+
forRun?(): Agent<TConfig, TResult>
|
|
41
|
+
|
|
20
42
|
cancel(): Promise<void>
|
|
21
43
|
getCapabilities(): AgentCapabilities
|
|
22
44
|
}
|
|
@@ -8,6 +8,26 @@ export type { AgentContextLevel } from './base.js'
|
|
|
8
8
|
export interface AgentDefinition {
|
|
9
9
|
info: AgentInfo
|
|
10
10
|
typedAgent: Agent<BaseAgentConfig, BaseAgentResult>
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build a fresh agent for a single spawn.
|
|
14
|
+
*
|
|
15
|
+
* `typedAgent` is ONE instance, and an instance refuses a second
|
|
16
|
+
* concurrent run because it holds per-run state. So a delegation fan-out
|
|
17
|
+
* naming the same `agent_id` four times ran one child and lost three to
|
|
18
|
+
* `ConcurrentInvocationError` — while `create_task`'s own description tells
|
|
19
|
+
* a model that exactly this fan-out is the thing to do.
|
|
20
|
+
*
|
|
21
|
+
* The manager prefers this over `typedAgent` for every spawn. Supply it
|
|
22
|
+
* when your agent needs real construction arguments; agents built on
|
|
23
|
+
* `AbstractAgent` already get a working default from `Agent.forRun`, so
|
|
24
|
+
* most hosts need nothing here.
|
|
25
|
+
*
|
|
26
|
+
* `configBuilder` is not a substitute: it produces a fresh CONFIG per
|
|
27
|
+
* spawn, and the config was never the part being shared.
|
|
28
|
+
*/
|
|
29
|
+
createAgent?: () => Agent<BaseAgentConfig, BaseAgentResult>
|
|
30
|
+
|
|
11
31
|
configBuilder?: (options: AgentFactoryOptions) => BaseAgentConfig | Promise<BaseAgentConfig>
|
|
12
32
|
|
|
13
33
|
contextLevel?: AgentContextLevel
|
package/src/types/hitl/index.ts
CHANGED
|
@@ -109,6 +109,24 @@ export interface PlanApprovalData {
|
|
|
109
109
|
id: string
|
|
110
110
|
description: string
|
|
111
111
|
toolName?: string
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Which agent the step is to be delegated to, when it is delegated.
|
|
115
|
+
*
|
|
116
|
+
* `PlanStep` gained this so an approver could see WHICH agent a step
|
|
117
|
+
* goes to rather than only THAT it delegates — approving "delegate
|
|
118
|
+
* this" is not approving "delegate this to the agent with shell
|
|
119
|
+
* access". It reached `PlanApprovalRequest`, which is the shape a host
|
|
120
|
+
* sees when it installs its own handler on `PlanManager`.
|
|
121
|
+
*
|
|
122
|
+
* It did not reach here, and this is the ordinary path: every host
|
|
123
|
+
* using `resumeHandler` is served by this type, and both mappers that
|
|
124
|
+
* build it copy field by field. So the fix landed on one of the two
|
|
125
|
+
* approval surfaces and the busier one kept showing
|
|
126
|
+
* `toolName: 'create_task'` and nothing else.
|
|
127
|
+
*/
|
|
128
|
+
agentId?: string
|
|
129
|
+
|
|
112
130
|
dependsOn: string[]
|
|
113
131
|
order: number
|
|
114
132
|
}>
|