@namzu/sdk 7.0.0 → 9.0.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 +652 -0
- package/dist/agents/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/SupervisorAgent.js +186 -163
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/agents/__tests__/a-policy-nobody-can-select.test.d.ts +2 -0
- package/dist/agents/__tests__/a-policy-nobody-can-select.test.d.ts.map +1 -0
- package/dist/agents/__tests__/a-policy-nobody-can-select.test.js +177 -0
- package/dist/agents/__tests__/a-policy-nobody-can-select.test.js.map +1 -0
- package/dist/agents/__tests__/supervisor-inbox-scope.test.d.ts +2 -0
- package/dist/agents/__tests__/supervisor-inbox-scope.test.d.ts.map +1 -0
- package/dist/agents/__tests__/supervisor-inbox-scope.test.js +125 -0
- package/dist/agents/__tests__/supervisor-inbox-scope.test.js.map +1 -0
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +4 -0
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +24 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/contracts/api.d.ts +1 -1
- package/dist/contracts/api.d.ts.map +1 -1
- package/dist/gateway/__tests__/a-knob-nobody-can-turn.test.d.ts +2 -0
- package/dist/gateway/__tests__/a-knob-nobody-can-turn.test.d.ts.map +1 -0
- package/dist/gateway/__tests__/a-knob-nobody-can-turn.test.js +114 -0
- package/dist/gateway/__tests__/a-knob-nobody-can-turn.test.js.map +1 -0
- package/dist/gateway/__tests__/completion-inbox.test.js +292 -2
- package/dist/gateway/__tests__/completion-inbox.test.js.map +1 -1
- package/dist/gateway/completion-inbox.d.ts +94 -6
- package/dist/gateway/completion-inbox.d.ts.map +1 -1
- package/dist/gateway/completion-inbox.js +235 -15
- package/dist/gateway/completion-inbox.js.map +1 -1
- package/dist/gateway/local.d.ts.map +1 -1
- package/dist/gateway/local.js +19 -6
- package/dist/gateway/local.js.map +1 -1
- package/dist/manager/agent/__tests__/lifecycle.test.js +56 -0
- package/dist/manager/agent/__tests__/lifecycle.test.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +62 -33
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.d.ts +2 -0
- package/dist/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.d.ts.map +1 -0
- package/dist/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.js +73 -0
- package/dist/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.js.map +1 -0
- package/dist/manager/plan/lifecycle.d.ts +48 -0
- package/dist/manager/plan/lifecycle.d.ts.map +1 -1
- package/dist/manager/plan/lifecycle.js +56 -0
- package/dist/manager/plan/lifecycle.js.map +1 -1
- package/dist/manager/run/persistence.d.ts +8 -0
- package/dist/manager/run/persistence.d.ts.map +1 -1
- package/dist/manager/run/persistence.js +12 -0
- package/dist/manager/run/persistence.js.map +1 -1
- package/dist/run/reporter.d.ts.map +1 -1
- package/dist/run/reporter.js +11 -0
- package/dist/run/reporter.js.map +1 -1
- package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.js +142 -0
- package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.js.map +1 -0
- package/dist/runtime/query/__tests__/completion-notification.test.js +414 -32
- package/dist/runtime/query/__tests__/completion-notification.test.js.map +1 -1
- package/dist/runtime/query/__tests__/context-size-on-the-wire.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/context-size-on-the-wire.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/context-size-on-the-wire.test.js +100 -0
- package/dist/runtime/query/__tests__/context-size-on-the-wire.test.js.map +1 -0
- package/dist/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.js +62 -0
- package/dist/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.js.map +1 -0
- package/dist/runtime/query/events.d.ts.map +1 -1
- package/dist/runtime/query/events.js +5 -0
- package/dist/runtime/query/events.js.map +1 -1
- package/dist/runtime/query/guard.d.ts +28 -0
- package/dist/runtime/query/guard.d.ts.map +1 -1
- package/dist/runtime/query/guard.js +31 -0
- package/dist/runtime/query/guard.js.map +1 -1
- package/dist/runtime/query/iteration/__tests__/settle-grace.test.d.ts +2 -0
- package/dist/runtime/query/iteration/__tests__/settle-grace.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/__tests__/settle-grace.test.js +226 -0
- package/dist/runtime/query/iteration/__tests__/settle-grace.test.js.map +1 -0
- package/dist/runtime/query/iteration/index.d.ts +92 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +814 -566
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.js +95 -0
- package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts +34 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +61 -4
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/store/task/disk.d.ts.map +1 -1
- package/dist/store/task/disk.js +14 -0
- package/dist/store/task/disk.js.map +1 -1
- package/dist/store/task/memory.d.ts.map +1 -1
- package/dist/store/task/memory.js +14 -0
- package/dist/store/task/memory.js.map +1 -1
- package/dist/tools/__tests__/untrusted-envelope.test.js +16 -0
- package/dist/tools/__tests__/untrusted-envelope.test.js.map +1 -1
- package/dist/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.js +101 -0
- package/dist/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.js.map +1 -0
- package/dist/tools/coordinator/__tests__/completion-delivery.test.js +117 -0
- package/dist/tools/coordinator/__tests__/completion-delivery.test.js.map +1 -1
- package/dist/tools/coordinator/__tests__/task-list.test.js +57 -0
- package/dist/tools/coordinator/__tests__/task-list.test.js.map +1 -1
- package/dist/tools/coordinator/__tests__/the-plan-a-human-approves.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/the-plan-a-human-approves.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/the-plan-a-human-approves.test.js +99 -0
- package/dist/tools/coordinator/__tests__/the-plan-a-human-approves.test.js.map +1 -0
- package/dist/tools/coordinator/agent.d.ts.map +1 -1
- package/dist/tools/coordinator/agent.js +17 -23
- package/dist/tools/coordinator/agent.js.map +1 -1
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +142 -14
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/coordinator/outcome.d.ts +60 -0
- package/dist/tools/coordinator/outcome.d.ts.map +1 -0
- package/dist/tools/coordinator/outcome.js +68 -0
- package/dist/tools/coordinator/outcome.js.map +1 -0
- package/dist/tools/untrusted-envelope.d.ts.map +1 -1
- package/dist/tools/untrusted-envelope.js +9 -1
- package/dist/tools/untrusted-envelope.js.map +1 -1
- package/dist/types/agent/gateway.d.ts +37 -2
- package/dist/types/agent/gateway.d.ts.map +1 -1
- package/dist/types/agent/supervisor.d.ts +38 -1
- package/dist/types/agent/supervisor.d.ts.map +1 -1
- package/dist/types/plan/index.d.ts +26 -0
- package/dist/types/plan/index.d.ts.map +1 -1
- package/dist/types/provider/interface.d.ts +35 -0
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/run/entity.d.ts +16 -0
- package/dist/types/run/entity.d.ts.map +1 -1
- package/dist/types/run/events.d.ts +92 -0
- package/dist/types/run/events.d.ts.map +1 -1
- package/dist/types/run/events.js.map +1 -1
- package/dist/types/run/prepare-step.d.ts +17 -2
- package/dist/types/run/prepare-step.d.ts.map +1 -1
- package/dist/types/task/index.d.ts +19 -1
- package/dist/types/task/index.d.ts.map +1 -1
- package/dist/types/task/index.js +10 -1
- package/dist/types/task/index.js.map +1 -1
- package/dist/types/verification/index.d.ts +98 -0
- package/dist/types/verification/index.d.ts.map +1 -1
- package/dist/types/verification/index.js +10 -0
- package/dist/types/verification/index.js.map +1 -1
- package/dist/verification/__tests__/argument-pattern.test.d.ts +2 -0
- package/dist/verification/__tests__/argument-pattern.test.d.ts.map +1 -0
- package/dist/verification/__tests__/argument-pattern.test.js +122 -0
- package/dist/verification/__tests__/argument-pattern.test.js.map +1 -0
- package/dist/verification/gate.d.ts.map +1 -1
- package/dist/verification/gate.js +44 -0
- package/dist/verification/gate.js.map +1 -1
- package/dist/verification/rules.d.ts.map +1 -1
- package/dist/verification/rules.js +27 -0
- package/dist/verification/rules.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/SupervisorAgent.ts +189 -167
- package/src/agents/__tests__/a-policy-nobody-can-select.test.ts +202 -0
- package/src/agents/__tests__/supervisor-inbox-scope.test.ts +149 -0
- package/src/bridge/a2a/mapper.ts +4 -0
- package/src/bridge/sse/mapper.ts +25 -0
- package/src/contracts/api.ts +1 -0
- package/src/gateway/__tests__/a-knob-nobody-can-turn.test.ts +139 -0
- package/src/gateway/__tests__/completion-inbox.test.ts +348 -2
- package/src/gateway/completion-inbox.ts +248 -16
- package/src/gateway/local.ts +19 -7
- package/src/manager/agent/__tests__/lifecycle.test.ts +74 -0
- package/src/manager/agent/lifecycle.ts +72 -42
- package/src/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.ts +88 -0
- package/src/manager/plan/lifecycle.ts +61 -0
- package/src/manager/run/persistence.ts +12 -0
- package/src/run/reporter.ts +12 -0
- package/src/runtime/query/__tests__/completion-does-not-erase-the-answer.test.ts +163 -0
- package/src/runtime/query/__tests__/completion-notification.test.ts +486 -34
- package/src/runtime/query/__tests__/context-size-on-the-wire.test.ts +122 -0
- package/src/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.ts +79 -0
- package/src/runtime/query/events.ts +5 -0
- package/src/runtime/query/guard.ts +32 -0
- package/src/runtime/query/iteration/__tests__/settle-grace.test.ts +265 -0
- package/src/runtime/query/iteration/index.ts +902 -636
- package/src/runtime/query/iteration/phases/__tests__/compaction-declined.test.ts +124 -0
- package/src/runtime/query/iteration/phases/compaction.ts +83 -10
- package/src/store/task/disk.ts +15 -0
- package/src/store/task/memory.ts +15 -0
- package/src/tools/__tests__/untrusted-envelope.test.ts +23 -0
- package/src/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.ts +133 -0
- package/src/tools/coordinator/__tests__/completion-delivery.test.ts +147 -0
- package/src/tools/coordinator/__tests__/task-list.test.ts +72 -0
- package/src/tools/coordinator/__tests__/the-plan-a-human-approves.test.ts +121 -0
- package/src/tools/coordinator/agent.ts +17 -25
- package/src/tools/coordinator/index.ts +155 -18
- package/src/tools/coordinator/outcome.ts +70 -0
- package/src/tools/untrusted-envelope.ts +9 -1
- package/src/types/agent/gateway.ts +37 -2
- package/src/types/agent/supervisor.ts +40 -1
- package/src/types/plan/index.ts +29 -0
- package/src/types/provider/interface.ts +39 -0
- package/src/types/run/entity.ts +17 -0
- package/src/types/run/events.ts +92 -0
- package/src/types/run/prepare-step.ts +17 -2
- package/src/types/task/index.ts +22 -3
- package/src/types/verification/index.ts +61 -0
- package/src/verification/__tests__/argument-pattern.test.ts +158 -0
- package/src/verification/gate.ts +45 -0
- package/src/verification/rules.ts +28 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { mkdtemp } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { describe, expect, it } from 'vitest'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
import { MockLLMProvider } from '../../provider/mock.js'
|
|
8
|
+
import { ToolNameCollisionError, ToolRegistry } from '../../registry/tool/execute.js'
|
|
9
|
+
import { defineTool } from '../../tools/defineTool.js'
|
|
10
|
+
import type { TaskGateway, TaskHandle } from '../../types/agent/gateway.js'
|
|
11
|
+
import type { TaskId } from '../../types/ids/index.js'
|
|
12
|
+
import { SupervisorAgent } from '../SupervisorAgent.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A gateway the HOST owns, which is the case that goes wrong.
|
|
16
|
+
*
|
|
17
|
+
* `SupervisorAgentConfig.gateway` is a first-class option, and a host that
|
|
18
|
+
* built a gateway reuses it — across sequential runs, and across concurrent
|
|
19
|
+
* ones. The supervisor attached a fresh `CompletionInbox` to it on every run
|
|
20
|
+
* and never detached, so the subscription set only grew: three runs, three
|
|
21
|
+
* live listeners, each still holding its own run's handles and each still
|
|
22
|
+
* being handed every other run's completions.
|
|
23
|
+
*/
|
|
24
|
+
class HostGateway implements TaskGateway {
|
|
25
|
+
readonly listeners = new Set<(h: TaskHandle) => void>()
|
|
26
|
+
|
|
27
|
+
async createTask(): Promise<TaskHandle> {
|
|
28
|
+
throw new Error('this test never launches')
|
|
29
|
+
}
|
|
30
|
+
async waitForTask(): Promise<TaskHandle> {
|
|
31
|
+
throw new Error('this test never waits')
|
|
32
|
+
}
|
|
33
|
+
async continueTask(): Promise<void> {}
|
|
34
|
+
cancelTask(): void {}
|
|
35
|
+
getTask(): TaskHandle | undefined {
|
|
36
|
+
return undefined
|
|
37
|
+
}
|
|
38
|
+
listTasks(): TaskHandle[] {
|
|
39
|
+
return []
|
|
40
|
+
}
|
|
41
|
+
onTaskCompleted(cb: (h: TaskHandle) => void): () => void {
|
|
42
|
+
this.listeners.add(cb)
|
|
43
|
+
return () => this.listeners.delete(cb)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A host tool whose name the coordinator surface also wants. */
|
|
48
|
+
const collidingTool = defineTool({
|
|
49
|
+
name: 'create_task',
|
|
50
|
+
description: 'a tool this host registered under a coordinator name',
|
|
51
|
+
inputSchema: z.object({}),
|
|
52
|
+
category: 'custom',
|
|
53
|
+
permissions: [],
|
|
54
|
+
readOnly: true,
|
|
55
|
+
destructive: false,
|
|
56
|
+
concurrencySafe: true,
|
|
57
|
+
async execute() {
|
|
58
|
+
return { success: true as const, output: 'never runs' }
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
async function runOnce(
|
|
63
|
+
gateway: TaskGateway,
|
|
64
|
+
id: string,
|
|
65
|
+
options: { collide?: boolean } = {},
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
const agent = new SupervisorAgent({
|
|
68
|
+
id,
|
|
69
|
+
name: 'Supervisor',
|
|
70
|
+
version: '1',
|
|
71
|
+
category: 'test',
|
|
72
|
+
description: 'coordinates workers',
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const tools = new ToolRegistry()
|
|
76
|
+
if (options.collide) tools.register(collidingTool)
|
|
77
|
+
|
|
78
|
+
await agent.run(
|
|
79
|
+
{
|
|
80
|
+
messages: [{ role: 'user', content: 'go', timestamp: 1 }],
|
|
81
|
+
workingDirectory: await mkdtemp(join(tmpdir(), 'namzu-inbox-scope-')),
|
|
82
|
+
} as never,
|
|
83
|
+
{
|
|
84
|
+
provider: new MockLLMProvider({ turns: [{ text: 'nothing to delegate' }] }),
|
|
85
|
+
agentIds: ['worker'],
|
|
86
|
+
gateway,
|
|
87
|
+
tools,
|
|
88
|
+
systemPrompt: 'You coordinate.',
|
|
89
|
+
model: 'mock-model',
|
|
90
|
+
tokenBudget: 100_000,
|
|
91
|
+
timeoutMs: 30_000,
|
|
92
|
+
maxIterations: 2,
|
|
93
|
+
sessionId: 'ses_scope',
|
|
94
|
+
threadId: 'thd_scope',
|
|
95
|
+
projectId: 'prj_scope',
|
|
96
|
+
tenantId: 'tnt_scope',
|
|
97
|
+
} as never,
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
describe('a supervisor releases the gateway it borrowed', () => {
|
|
102
|
+
it('leaves no listener behind, however many runs the host makes', async () => {
|
|
103
|
+
const gateway = new HostGateway()
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < 3; i++) {
|
|
106
|
+
await runOnce(gateway, `sup_${i}`)
|
|
107
|
+
expect(gateway.listeners.size, `run ${i + 1} left its completion listener attached`).toBe(0)
|
|
108
|
+
}
|
|
109
|
+
}, 60_000)
|
|
110
|
+
|
|
111
|
+
it('releases it when setup throws before the run ever starts', async () => {
|
|
112
|
+
// The reason it is a `finally` covering the whole body and not a line
|
|
113
|
+
// after `drainQuery`. A host whose tool shares a coordinator name gets
|
|
114
|
+
// `ToolNameCollisionError` from the registration loop — after the inbox
|
|
115
|
+
// attached — then fixes its config and runs again. A leak of one
|
|
116
|
+
// listener per run becomes one per ATTEMPT, and the attempts are what
|
|
117
|
+
// there are most of.
|
|
118
|
+
const gateway = new HostGateway()
|
|
119
|
+
|
|
120
|
+
await expect(runOnce(gateway, 'sup_collide', { collide: true })).rejects.toThrow(
|
|
121
|
+
ToolNameCollisionError,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
expect(gateway.listeners.size, 'a run that threw left its listener attached').toBe(0)
|
|
125
|
+
}, 60_000)
|
|
126
|
+
|
|
127
|
+
it('never hands a completion to a run that did not launch it', async () => {
|
|
128
|
+
// Two supervisors, one gateway. The second run's inbox is gone by the
|
|
129
|
+
// time this fires, but the assertion that matters is the one above it:
|
|
130
|
+
// nothing is listening that should not be.
|
|
131
|
+
const gateway = new HostGateway()
|
|
132
|
+
await runOnce(gateway, 'sup_a')
|
|
133
|
+
await runOnce(gateway, 'sup_b')
|
|
134
|
+
|
|
135
|
+
let delivered = 0
|
|
136
|
+
for (const cb of gateway.listeners) {
|
|
137
|
+
delivered += 1
|
|
138
|
+
cb({
|
|
139
|
+
taskId: 'tsk_foreign' as TaskId,
|
|
140
|
+
agentId: 'worker',
|
|
141
|
+
state: 'completed',
|
|
142
|
+
createdAt: 0,
|
|
143
|
+
completedAt: 1,
|
|
144
|
+
} as TaskHandle)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
expect(delivered).toBe(0)
|
|
148
|
+
}, 60_000)
|
|
149
|
+
})
|
package/src/bridge/a2a/mapper.ts
CHANGED
|
@@ -188,6 +188,10 @@ const MAPPING: {
|
|
|
188
188
|
// Context management is kernel-internal bookkeeping; A2A peers model a
|
|
189
189
|
// task lifecycle, not the host runtime's memory strategy.
|
|
190
190
|
compaction_completed: null,
|
|
191
|
+
// Compaction, succeeded or declined, is a property of how this runtime
|
|
192
|
+
// manages its own context. A peer models a task lifecycle and cannot act on
|
|
193
|
+
// either outcome.
|
|
194
|
+
compaction_failed: null,
|
|
191
195
|
// A refusal is the run's own policy decision; the peer sees it in the
|
|
192
196
|
// terminal task state, not as a separate signal.
|
|
193
197
|
guardrail_triggered: null,
|
package/src/bridge/sse/mapper.ts
CHANGED
|
@@ -92,6 +92,21 @@ const MAPPING: {
|
|
|
92
92
|
}),
|
|
93
93
|
},
|
|
94
94
|
|
|
95
|
+
// Carried for the same reason its sibling is: a host that can show a user
|
|
96
|
+
// context was dropped must also be able to show them it was not, because a
|
|
97
|
+
// run continuing at full context is the state that ends in an opaque
|
|
98
|
+
// provider rejection later.
|
|
99
|
+
compaction_failed: {
|
|
100
|
+
wire: 'compaction.failed',
|
|
101
|
+
transform: (e, runId) => ({
|
|
102
|
+
run_id: runId,
|
|
103
|
+
iteration: e.iteration,
|
|
104
|
+
cause: e.cause,
|
|
105
|
+
messages: e.messages,
|
|
106
|
+
...(e.error !== undefined ? { error: e.error } : {}),
|
|
107
|
+
}),
|
|
108
|
+
},
|
|
109
|
+
|
|
95
110
|
tool_executing: {
|
|
96
111
|
wire: 'tool.executing',
|
|
97
112
|
transform: (e, runId) => ({
|
|
@@ -212,6 +227,16 @@ const MAPPING: {
|
|
|
212
227
|
run_id: runId,
|
|
213
228
|
usage: e.usage,
|
|
214
229
|
cost: e.cost,
|
|
230
|
+
// Carried, and named apart from `usage` on the wire as well as in
|
|
231
|
+
// the type. A remote surface has exactly the same opportunity to
|
|
232
|
+
// divide cumulative spend by a context window as a local one, and
|
|
233
|
+
// no more information with which to notice.
|
|
234
|
+
...(e.contextTokens !== undefined ? { context_tokens: e.contextTokens } : {}),
|
|
235
|
+
...(e.contextMeasuredBy !== undefined ? { context_measured_by: e.contextMeasuredBy } : {}),
|
|
236
|
+
...(e.contextWindowTokens !== undefined
|
|
237
|
+
? { context_window_tokens: e.contextWindowTokens }
|
|
238
|
+
: {}),
|
|
239
|
+
...(e.windowSource !== undefined ? { window_source: e.windowSource } : {}),
|
|
215
240
|
}),
|
|
216
241
|
},
|
|
217
242
|
|
package/src/contracts/api.ts
CHANGED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { Span } from '@opentelemetry/api'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import type { Agent } from '../../types/agent/core.js'
|
|
5
|
+
import type { AgentManagerContract } from '../../types/agent/manager.js'
|
|
6
|
+
import type {
|
|
7
|
+
AgentTask,
|
|
8
|
+
AgentTaskContext,
|
|
9
|
+
AgentTaskState,
|
|
10
|
+
SendMessageOptions,
|
|
11
|
+
} from '../../types/agent/task.js'
|
|
12
|
+
import type { AgentId, RunId, SessionId, TaskId, TenantId } from '../../types/ids/index.js'
|
|
13
|
+
import type { ProjectId, ThreadId } from '../../types/session/ids.js'
|
|
14
|
+
import { LocalTaskGateway } from '../local.js'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `CreateTaskOptions.configOverrides` was declared, typed, and never read.
|
|
18
|
+
*
|
|
19
|
+
* `createTask` built its OWN `configOverrides` object out of `parentSpan`
|
|
20
|
+
* alone, so a caller pinning a delegated run to a cheaper model, or capping
|
|
21
|
+
* its iterations, got the agent's defaults and nothing to say otherwise. The
|
|
22
|
+
* field type-checked, the call succeeded, and the run was not the run that was
|
|
23
|
+
* asked for.
|
|
24
|
+
*
|
|
25
|
+
* Reachability, not behaviour: what `configOverrides` DOES once it lands on
|
|
26
|
+
* `sendMessage` is the agent manager's business and is tested there. What was
|
|
27
|
+
* broken is the hop.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** Records what the gateway actually asked the manager for. */
|
|
31
|
+
class RecordingManager implements AgentManagerContract {
|
|
32
|
+
readonly sent: SendMessageOptions[] = []
|
|
33
|
+
|
|
34
|
+
async sendMessage(options: SendMessageOptions): Promise<AgentTask> {
|
|
35
|
+
this.sent.push(options)
|
|
36
|
+
return {
|
|
37
|
+
taskId: `task_${this.sent.length}` as TaskId,
|
|
38
|
+
agentId: options.agentId,
|
|
39
|
+
agent: {} as Agent<never, never>,
|
|
40
|
+
childAbortController: new AbortController(),
|
|
41
|
+
context: {} as AgentTaskContext,
|
|
42
|
+
state: 'completed' as AgentTaskState,
|
|
43
|
+
pendingMessages: [],
|
|
44
|
+
createdAt: 1,
|
|
45
|
+
} as AgentTask
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
cancel(): void {}
|
|
49
|
+
cancelAll(): void {}
|
|
50
|
+
async continueTask(): Promise<void> {}
|
|
51
|
+
queueMessage(): void {}
|
|
52
|
+
drainMessages() {
|
|
53
|
+
return []
|
|
54
|
+
}
|
|
55
|
+
async waitForCompletion(): Promise<void> {}
|
|
56
|
+
getInstance(): AgentTask | undefined {
|
|
57
|
+
return undefined
|
|
58
|
+
}
|
|
59
|
+
listByParent(): AgentTask[] {
|
|
60
|
+
return []
|
|
61
|
+
}
|
|
62
|
+
listActive(): AgentTask[] {
|
|
63
|
+
return []
|
|
64
|
+
}
|
|
65
|
+
getState(): AgentTaskState | undefined {
|
|
66
|
+
return undefined
|
|
67
|
+
}
|
|
68
|
+
on(): void {}
|
|
69
|
+
off(): void {}
|
|
70
|
+
cleanup(): void {}
|
|
71
|
+
dispose(): void {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function context(): AgentTaskContext {
|
|
75
|
+
return {
|
|
76
|
+
parentRunId: 'run_parent' as RunId,
|
|
77
|
+
parentAgentId: 'supervisor',
|
|
78
|
+
parentAbortController: new AbortController(),
|
|
79
|
+
depth: 0,
|
|
80
|
+
budgetTracker: { total: 100_000, remaining: 100_000 },
|
|
81
|
+
tenantId: 'tnt_k' as TenantId,
|
|
82
|
+
threadId: 'thd_k' as ThreadId,
|
|
83
|
+
sessionId: 'ses_k' as SessionId,
|
|
84
|
+
projectId: 'prj_k' as ProjectId,
|
|
85
|
+
parentActor: { kind: 'agent', agentId: 'supervisor' as AgentId, tenantId: 'tnt_k' as TenantId },
|
|
86
|
+
} as AgentTaskContext
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe('a delegated run is built with the config its caller asked for', () => {
|
|
90
|
+
it('forwards configOverrides to the spawn', async () => {
|
|
91
|
+
const manager = new RecordingManager()
|
|
92
|
+
const gateway = new LocalTaskGateway(manager, context())
|
|
93
|
+
|
|
94
|
+
await gateway.createTask({
|
|
95
|
+
agentId: 'worker',
|
|
96
|
+
prompt: 'work',
|
|
97
|
+
workingDirectory: '/tmp',
|
|
98
|
+
configOverrides: { model: 'cheap-model', maxIterations: 3 },
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
expect(manager.sent[0]?.configOverrides).toMatchObject({
|
|
102
|
+
model: 'cheap-model',
|
|
103
|
+
maxIterations: 3,
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('keeps the dedicated parentSpan option winning when both name a span', async () => {
|
|
108
|
+
// A caller who sets both is saying the same thing twice, and the named
|
|
109
|
+
// field is the specific one for the job — so it is applied last.
|
|
110
|
+
const manager = new RecordingManager()
|
|
111
|
+
const gateway = new LocalTaskGateway(manager, context())
|
|
112
|
+
const named = { spanContext: () => ({ traceId: 'named' }) } as unknown as Span
|
|
113
|
+
const buried = { spanContext: () => ({ traceId: 'buried' }) } as unknown as Span
|
|
114
|
+
|
|
115
|
+
await gateway.createTask({
|
|
116
|
+
agentId: 'worker',
|
|
117
|
+
prompt: 'work',
|
|
118
|
+
workingDirectory: '/tmp',
|
|
119
|
+
parentSpan: named,
|
|
120
|
+
configOverrides: { parentSpan: buried, model: 'cheap-model' },
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
expect(manager.sent[0]?.configOverrides?.parentSpan).toBe(named)
|
|
124
|
+
// ...and the rest of the caller's overrides survive alongside it.
|
|
125
|
+
expect(manager.sent[0]?.configOverrides?.model).toBe('cheap-model')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('still sends no configOverrides at all when the caller set neither', async () => {
|
|
129
|
+
// The absent case has to stay absent: an empty object here would put
|
|
130
|
+
// `configOverrides: {}` on every spawn and override nothing, which is
|
|
131
|
+
// harmless until something starts reading its presence as intent.
|
|
132
|
+
const manager = new RecordingManager()
|
|
133
|
+
const gateway = new LocalTaskGateway(manager, context())
|
|
134
|
+
|
|
135
|
+
await gateway.createTask({ agentId: 'worker', prompt: 'work', workingDirectory: '/tmp' })
|
|
136
|
+
|
|
137
|
+
expect(manager.sent[0]).not.toHaveProperty('configOverrides')
|
|
138
|
+
})
|
|
139
|
+
})
|