@namzu/sdk 6.2.0 → 8.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 +677 -0
- package/dist/agents/ReactiveAgent.d.ts.map +1 -1
- package/dist/agents/ReactiveAgent.js +5 -0
- package/dist/agents/ReactiveAgent.js.map +1 -1
- package/dist/agents/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/SupervisorAgent.js +172 -158
- package/dist/agents/SupervisorAgent.js.map +1 -1
- 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/agents/runAgent.d.ts +19 -1
- package/dist/agents/runAgent.d.ts.map +1 -1
- package/dist/agents/runAgent.js +2 -0
- package/dist/agents/runAgent.js.map +1 -1
- 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/connector/mcp/__tests__/positional-arrays.test.d.ts +2 -0
- package/dist/connector/mcp/__tests__/positional-arrays.test.d.ts.map +1 -0
- package/dist/connector/mcp/__tests__/positional-arrays.test.js +142 -0
- package/dist/connector/mcp/__tests__/positional-arrays.test.js.map +1 -0
- package/dist/connector/mcp/adapter.d.ts.map +1 -1
- package/dist/connector/mcp/adapter.js +123 -8
- package/dist/connector/mcp/adapter.js.map +1 -1
- package/dist/constants/agent/index.d.ts +5 -0
- package/dist/constants/agent/index.d.ts.map +1 -1
- package/dist/constants/agent/index.js +5 -0
- package/dist/constants/agent/index.js.map +1 -1
- package/dist/constants/plugin/index.d.ts +15 -0
- package/dist/constants/plugin/index.d.ts.map +1 -1
- package/dist/constants/plugin/index.js +15 -0
- package/dist/constants/plugin/index.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__/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 +11 -0
- package/dist/gateway/local.d.ts.map +1 -1
- package/dist/gateway/local.js +27 -1
- package/dist/gateway/local.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +6 -0
- package/dist/manager/agent/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/provider/thinking-support.d.ts +2 -1
- package/dist/provider/thinking-support.d.ts.map +1 -1
- package/dist/provider/thinking-support.js +14 -0
- package/dist/provider/thinking-support.js.map +1 -1
- package/dist/public-runtime.d.ts +1 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +9 -1
- package/dist/public-runtime.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__/context.test.js +18 -0
- package/dist/runtime/query/__tests__/context.test.js.map +1 -1
- package/dist/runtime/query/__tests__/effort-reaches-the-wire.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/effort-reaches-the-wire.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/effort-reaches-the-wire.test.js +118 -0
- package/dist/runtime/query/__tests__/effort-reaches-the-wire.test.js.map +1 -0
- package/dist/runtime/query/__tests__/tool-timeout.test.js +34 -0
- package/dist/runtime/query/__tests__/tool-timeout.test.js.map +1 -1
- package/dist/runtime/query/context.d.ts.map +1 -1
- package/dist/runtime/query/context.js +16 -1
- package/dist/runtime/query/context.js.map +1 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +11 -1
- package/dist/runtime/query/executor.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 +818 -565
- 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/telemetry/__tests__/model-call-span.test.js +22 -4
- package/dist/telemetry/__tests__/model-call-span.test.js.map +1 -1
- package/dist/telemetry/__tests__/span-closure.test.js +12 -5
- package/dist/telemetry/__tests__/span-closure.test.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__/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__/wait-with-idle-bound.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/wait-with-idle-bound.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/wait-with-idle-bound.test.js +193 -0
- package/dist/tools/coordinator/__tests__/wait-with-idle-bound.test.js.map +1 -0
- package/dist/tools/coordinator/index.d.ts +19 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +191 -71
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/coordinator/wait-with-idle-bound.d.ts +66 -0
- package/dist/tools/coordinator/wait-with-idle-bound.d.ts.map +1 -0
- package/dist/tools/coordinator/wait-with-idle-bound.js +78 -0
- package/dist/tools/coordinator/wait-with-idle-bound.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/base.d.ts +16 -0
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/agent/gateway.d.ts +41 -0
- package/dist/types/agent/gateway.d.ts.map +1 -1
- package/dist/types/agent/lifecycle-event.d.ts +9 -1
- package/dist/types/agent/lifecycle-event.d.ts.map +1 -1
- package/dist/types/agent/task.d.ts +5 -0
- package/dist/types/agent/task.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +10 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/probe/registry.d.ts +6 -0
- package/dist/types/probe/registry.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/config.d.ts +25 -0
- package/dist/types/run/config.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 +75 -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/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/utils/__tests__/abort-reason.test.d.ts +2 -0
- package/dist/utils/__tests__/abort-reason.test.d.ts.map +1 -0
- package/dist/utils/__tests__/abort-reason.test.js +48 -0
- package/dist/utils/__tests__/abort-reason.test.js.map +1 -0
- package/dist/utils/abort.d.ts +26 -0
- package/dist/utils/abort.d.ts.map +1 -1
- package/dist/utils/abort.js +34 -0
- package/dist/utils/abort.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/__tests__/rule-order-and-reason.test.d.ts +2 -0
- package/dist/verification/__tests__/rule-order-and-reason.test.d.ts.map +1 -0
- package/dist/verification/__tests__/rule-order-and-reason.test.js +126 -0
- package/dist/verification/__tests__/rule-order-and-reason.test.js.map +1 -0
- package/dist/verification/gate.d.ts +17 -1
- package/dist/verification/gate.d.ts.map +1 -1
- package/dist/verification/gate.js +102 -2
- package/dist/verification/gate.js.map +1 -1
- package/dist/verification/index.d.ts +1 -1
- package/dist/verification/index.d.ts.map +1 -1
- package/dist/verification/index.js +1 -1
- package/dist/verification/index.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/ReactiveAgent.ts +5 -0
- package/src/agents/SupervisorAgent.ts +175 -162
- package/src/agents/__tests__/supervisor-inbox-scope.test.ts +149 -0
- package/src/agents/runAgent.ts +22 -1
- package/src/bridge/a2a/mapper.ts +4 -0
- package/src/bridge/sse/mapper.ts +25 -0
- package/src/connector/mcp/__tests__/positional-arrays.test.ts +183 -0
- package/src/connector/mcp/adapter.ts +131 -7
- package/src/constants/agent/index.ts +5 -0
- package/src/constants/plugin/index.ts +15 -0
- package/src/contracts/api.ts +1 -0
- package/src/gateway/__tests__/completion-inbox.test.ts +348 -2
- package/src/gateway/completion-inbox.ts +248 -16
- package/src/gateway/local.ts +26 -1
- package/src/manager/agent/lifecycle.ts +6 -0
- package/src/manager/run/persistence.ts +12 -0
- package/src/provider/thinking-support.ts +19 -2
- package/src/public-runtime.ts +9 -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__/context.test.ts +24 -0
- package/src/runtime/query/__tests__/effort-reaches-the-wire.test.ts +135 -0
- package/src/runtime/query/__tests__/tool-timeout.test.ts +38 -0
- package/src/runtime/query/context.ts +16 -1
- package/src/runtime/query/executor.ts +11 -1
- 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 +906 -635
- 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/telemetry/__tests__/model-call-span.test.ts +22 -5
- package/src/telemetry/__tests__/span-closure.test.ts +12 -5
- package/src/tools/__tests__/untrusted-envelope.test.ts +23 -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__/wait-with-idle-bound.test.ts +247 -0
- package/src/tools/coordinator/index.ts +205 -78
- package/src/tools/coordinator/wait-with-idle-bound.ts +142 -0
- package/src/tools/untrusted-envelope.ts +9 -1
- package/src/types/agent/base.ts +17 -0
- package/src/types/agent/gateway.ts +42 -0
- package/src/types/agent/lifecycle-event.ts +7 -0
- package/src/types/agent/task.ts +5 -0
- package/src/types/hitl/index.ts +10 -0
- package/src/types/probe/registry.ts +6 -0
- package/src/types/provider/interface.ts +39 -0
- package/src/types/run/config.ts +26 -0
- package/src/types/run/entity.ts +17 -0
- package/src/types/run/events.ts +75 -0
- package/src/types/run/prepare-step.ts +17 -2
- package/src/types/verification/index.ts +61 -0
- package/src/utils/__tests__/abort-reason.test.ts +56 -0
- package/src/utils/abort.ts +34 -0
- package/src/verification/__tests__/argument-pattern.test.ts +158 -0
- package/src/verification/__tests__/rule-order-and-reason.test.ts +149 -0
- package/src/verification/gate.ts +106 -3
- package/src/verification/index.ts +1 -1
- package/src/verification/rules.ts +28 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import { MockLLMProvider } from '../../../provider/mock.js'
|
|
6
|
+
import { ToolRegistry } from '../../../registry/tool/execute.js'
|
|
7
|
+
import { createUserMessage } from '../../../types/message/index.js'
|
|
8
|
+
import type { RunEvent } from '../../../types/run/index.js'
|
|
9
|
+
import { drainQuery } from '../index.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Cumulative spend and current context size are different quantities, and a
|
|
13
|
+
* host divided the first by a context window and shipped it.
|
|
14
|
+
*
|
|
15
|
+
* That indicator climbed toward full on any long run no matter how much room
|
|
16
|
+
* the conversation actually had — most wrong exactly when someone needed it —
|
|
17
|
+
* because `usage` is summed over every turn and never falls, while the context
|
|
18
|
+
* is what is being sent right now and falls whenever a compaction sheds.
|
|
19
|
+
*
|
|
20
|
+
* The host had no better option: the correct numerator needs internals it
|
|
21
|
+
* cannot see, and the correct denominator was a two-branch guess on a model
|
|
22
|
+
* name. So both numbers are on the event, named so that reaching for the wrong
|
|
23
|
+
* one is a visible mistake rather than a plausible guess.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
let workdirs: string[] = []
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
await Promise.all(workdirs.map((d) => rm(d, { recursive: true, force: true })))
|
|
30
|
+
workdirs = []
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
type UsageEvent = Extract<RunEvent, { type: 'token_usage_updated' }>
|
|
34
|
+
|
|
35
|
+
async function run(
|
|
36
|
+
compaction: Record<string, unknown> | undefined,
|
|
37
|
+
turns: unknown[],
|
|
38
|
+
): Promise<UsageEvent[]> {
|
|
39
|
+
const seen: UsageEvent[] = []
|
|
40
|
+
const dir = await mkdtemp(join(tmpdir(), 'namzu-ctxsize-'))
|
|
41
|
+
workdirs.push(dir)
|
|
42
|
+
|
|
43
|
+
await drainQuery(
|
|
44
|
+
{
|
|
45
|
+
provider: new MockLLMProvider({ turns: turns as never }),
|
|
46
|
+
tools: new ToolRegistry(),
|
|
47
|
+
runConfig: { model: 'mock-model', timeoutMs: 30_000, tokenBudget: 100_000, maxIterations: 3 },
|
|
48
|
+
...(compaction ? { compactionConfig: compaction } : {}),
|
|
49
|
+
agentId: 'agent_ctx',
|
|
50
|
+
agentName: 'Context Agent',
|
|
51
|
+
workingDirectory: dir,
|
|
52
|
+
sessionId: 'ses_ctx',
|
|
53
|
+
threadId: 'thd_ctx',
|
|
54
|
+
projectId: 'prj_ctx',
|
|
55
|
+
tenantId: 'tnt_ctx',
|
|
56
|
+
messages: [createUserMessage('go')],
|
|
57
|
+
} as never,
|
|
58
|
+
(event: RunEvent) => {
|
|
59
|
+
if (event.type === 'token_usage_updated') seen.push(event)
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return seen
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const COMPACTION = {
|
|
67
|
+
strategy: 'sliding-window',
|
|
68
|
+
triggerThreshold: 0.9,
|
|
69
|
+
contextWindowTokens: 50_000,
|
|
70
|
+
keepRecentMessages: 4,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe('a run reports how much room its context has', () => {
|
|
74
|
+
it('carries the context size and the window together', async () => {
|
|
75
|
+
const events = await run(COMPACTION, [{ text: 'done' }])
|
|
76
|
+
|
|
77
|
+
expect(events.length).toBeGreaterThan(0)
|
|
78
|
+
const event = events[0] as UsageEvent
|
|
79
|
+
expect(event.contextTokens).toBeGreaterThan(0)
|
|
80
|
+
expect(event.contextWindowTokens).toBe(50_000)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('says whether each number was measured or guessed', async () => {
|
|
84
|
+
// A fraction is only as honest as the weaker of its two terms, so a
|
|
85
|
+
// surface rendering these owes a reader the distinction. It cannot
|
|
86
|
+
// pass it on if it never receives it.
|
|
87
|
+
const event = (await run(COMPACTION, [{ text: 'done' }]))[0] as UsageEvent
|
|
88
|
+
|
|
89
|
+
expect(['provider', 'estimate']).toContain(event.contextMeasuredBy)
|
|
90
|
+
// The window was stated in config here, so its provenance is not a guess.
|
|
91
|
+
expect(event.windowSource).toBe('config')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('keeps the context size distinct from cumulative spend', async () => {
|
|
95
|
+
// The whole defect in one assertion, and this driver demonstrates it
|
|
96
|
+
// more sharply than a real one would: it reports no usage at all, so
|
|
97
|
+
// cumulative spend stays at zero while the context genuinely fills.
|
|
98
|
+
//
|
|
99
|
+
// A surface dividing spend by a window would show 0% here, for a
|
|
100
|
+
// conversation that is really there. The same surface on a long run
|
|
101
|
+
// shows 100% for a conversation that was compacted down to nothing.
|
|
102
|
+
// Both directions, same category error — which is why the two numbers
|
|
103
|
+
// are reported separately rather than left to be inferred from each
|
|
104
|
+
// other.
|
|
105
|
+
const events = await run(COMPACTION, [{ text: 'one' }, { text: 'two' }])
|
|
106
|
+
const last = events[events.length - 1] as UsageEvent
|
|
107
|
+
|
|
108
|
+
expect(last.contextTokens).toBeGreaterThan(0)
|
|
109
|
+
expect(last.contextTokens).not.toBe(last.usage.totalTokens)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('reports nothing rather than a guess when no window is configured', async () => {
|
|
113
|
+
// Without a compaction config nothing resolves a window, and inventing
|
|
114
|
+
// one would be the guess this replaces. Absent is a fact a surface can
|
|
115
|
+
// act on; a fabricated default is not.
|
|
116
|
+
const event = (await run(undefined, [{ text: 'done' }]))[0] as UsageEvent
|
|
117
|
+
|
|
118
|
+
expect(event.contextTokens).toBeUndefined()
|
|
119
|
+
expect(event.contextWindowTokens).toBeUndefined()
|
|
120
|
+
expect(event.windowSource).toBeUndefined()
|
|
121
|
+
})
|
|
122
|
+
})
|
|
@@ -100,4 +100,28 @@ describe('RunContextFactory.build', () => {
|
|
|
100
100
|
const runDir = builder.runDir('prj_x' as ProjectId, 'ses_y' as SessionId, 'run_z' as RunId)
|
|
101
101
|
expect(posix(runDir)).toBe('/base/.namzu/projects/prj_x/sessions/ses_y/runs/run_z')
|
|
102
102
|
})
|
|
103
|
+
|
|
104
|
+
it("carries the caller's stop reason across into the run", () => {
|
|
105
|
+
// This is the frame that was losing it. The host aborts with a
|
|
106
|
+
// sentence, the run re-aborts with nothing, and every layer below —
|
|
107
|
+
// the tool executor, the tool itself, the result the model reads —
|
|
108
|
+
// can only report that something stopped. The words do not survive
|
|
109
|
+
// the hop unless this site forwards them.
|
|
110
|
+
const host = new AbortController()
|
|
111
|
+
const ctx = RunContextFactory.build(buildConfig({ signal: host.signal }))
|
|
112
|
+
|
|
113
|
+
host.abort(new Error('nightly window closed'))
|
|
114
|
+
|
|
115
|
+
expect(ctx.abortController.signal.aborted).toBe(true)
|
|
116
|
+
expect((ctx.abortController.signal.reason as Error)?.message).toBe('nightly window closed')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('still aborts when the caller gave no reason', () => {
|
|
120
|
+
const host = new AbortController()
|
|
121
|
+
const ctx = RunContextFactory.build(buildConfig({ signal: host.signal }))
|
|
122
|
+
|
|
123
|
+
host.abort()
|
|
124
|
+
|
|
125
|
+
expect(ctx.abortController.signal.aborted).toBe(true)
|
|
126
|
+
})
|
|
103
127
|
})
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import { MockLLMProvider } from '../../../provider/mock.js'
|
|
6
|
+
import { ToolRegistry } from '../../../registry/tool/execute.js'
|
|
7
|
+
import { createUserMessage } from '../../../types/message/index.js'
|
|
8
|
+
import type { AgentRunConfig } from '../../../types/run/index.js'
|
|
9
|
+
import { drainQuery } from '../index.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `effort` was declared on the provider params, exported, and read by a driver
|
|
13
|
+
* that wrote it straight to the wire — and nothing in the kernel ever set it.
|
|
14
|
+
* No caller could reach it, and the symptom (every request going out at the
|
|
15
|
+
* model's default) reads as "this model ignores effort" rather than "nobody
|
|
16
|
+
* plumbed it through".
|
|
17
|
+
*
|
|
18
|
+
* These drive a real run and read what the provider was actually handed, which
|
|
19
|
+
* is the only thing that distinguishes a wired field from a declared one. A
|
|
20
|
+
* test asserting the field exists on the config type would have passed against
|
|
21
|
+
* the broken version.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
let workdirs: string[] = []
|
|
25
|
+
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
await Promise.all(workdirs.map((d) => rm(d, { recursive: true, force: true })))
|
|
28
|
+
workdirs = []
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
async function run(overrides: Partial<AgentRunConfig>, turns: unknown[]): Promise<MockLLMProvider> {
|
|
32
|
+
const provider = new MockLLMProvider({ turns: turns as never })
|
|
33
|
+
const dir = await mkdtemp(join(tmpdir(), 'namzu-effort-'))
|
|
34
|
+
workdirs.push(dir)
|
|
35
|
+
|
|
36
|
+
await drainQuery({
|
|
37
|
+
provider,
|
|
38
|
+
tools: new ToolRegistry(),
|
|
39
|
+
runConfig: {
|
|
40
|
+
model: 'mock-model',
|
|
41
|
+
timeoutMs: 30_000,
|
|
42
|
+
tokenBudget: 100_000,
|
|
43
|
+
maxIterations: 2,
|
|
44
|
+
...overrides,
|
|
45
|
+
},
|
|
46
|
+
agentId: 'agent_effort',
|
|
47
|
+
agentName: 'Effort Agent',
|
|
48
|
+
workingDirectory: dir,
|
|
49
|
+
sessionId: 'ses_effort',
|
|
50
|
+
threadId: 'thd_effort',
|
|
51
|
+
projectId: 'prj_effort',
|
|
52
|
+
tenantId: 'tnt_effort',
|
|
53
|
+
messages: [createUserMessage('go')],
|
|
54
|
+
} as never)
|
|
55
|
+
|
|
56
|
+
return provider
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('an effort level set on the run reaches the provider', () => {
|
|
60
|
+
it('arrives on the request', async () => {
|
|
61
|
+
const provider = await run({ effort: 'max' }, [{ text: 'done' }])
|
|
62
|
+
|
|
63
|
+
expect(provider.requests.length).toBeGreaterThan(0)
|
|
64
|
+
expect(provider.requests[0]?.effort).toBe('max')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('is absent when nobody asked for one', async () => {
|
|
68
|
+
// Not `undefined`-valued but genuinely absent: a present key carrying
|
|
69
|
+
// undefined is the kind of thing that survives a spread into a request
|
|
70
|
+
// body and reaches a wire that did not expect the field.
|
|
71
|
+
const provider = await run({}, [{ text: 'done' }])
|
|
72
|
+
|
|
73
|
+
expect(provider.requests[0] && 'effort' in provider.requests[0]).toBe(false)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('rides every turn of the run, not only the first', async () => {
|
|
77
|
+
// The value is run-level because the provider documents that changing
|
|
78
|
+
// it between requests invalidates the cached prefix. A run that
|
|
79
|
+
// forwarded it once and then stopped would pay that cost silently.
|
|
80
|
+
const provider = await run({ effort: 'low' }, [{ text: 'one' }, { text: 'two' }])
|
|
81
|
+
|
|
82
|
+
for (const request of provider.requests) {
|
|
83
|
+
expect(request.effort).toBe('low')
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('travels alongside thinking rather than inside it', async () => {
|
|
88
|
+
const provider = await run({ effort: 'high', thinking: { type: 'adaptive' } }, [
|
|
89
|
+
{ text: 'done' },
|
|
90
|
+
])
|
|
91
|
+
|
|
92
|
+
expect(provider.requests[0]?.effort).toBe('high')
|
|
93
|
+
expect(provider.requests[0]?.thinking?.type).toBe('adaptive')
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('the front door forwards it too, not only the kernel', () => {
|
|
98
|
+
/**
|
|
99
|
+
* These exist because everything above passed while a real run put NOTHING
|
|
100
|
+
* on the wire.
|
|
101
|
+
*
|
|
102
|
+
* `drainQuery` takes the run config a caller hands it, so testing through
|
|
103
|
+
* it proves the loop forwards the field and nothing about whether a caller
|
|
104
|
+
* can set it. Every ergonomic entry point — this one, `ReactiveAgent`,
|
|
105
|
+
* `SupervisorAgent`, and the manager's bare-config branch — builds its
|
|
106
|
+
* `AgentRunConfig` by HAND-LISTING fields, so a field nobody remembered to
|
|
107
|
+
* add is dropped in silence, with no cast to blame and no error to see.
|
|
108
|
+
* `thinking` had been in that state since it shipped.
|
|
109
|
+
*
|
|
110
|
+
* It was found by watching an actual HTTP body, which is the only place the
|
|
111
|
+
* gap is visible. So the regression test drives the front door.
|
|
112
|
+
*/
|
|
113
|
+
it('reaches the provider through runAgent', async () => {
|
|
114
|
+
const { runAgent } = await import('../../../agents/runAgent.js')
|
|
115
|
+
const provider = new MockLLMProvider({ turns: [{ text: 'done' }] as never })
|
|
116
|
+
const dir = await mkdtemp(join(tmpdir(), 'namzu-effort-door-'))
|
|
117
|
+
workdirs.push(dir)
|
|
118
|
+
|
|
119
|
+
await runAgent({
|
|
120
|
+
provider,
|
|
121
|
+
model: 'mock-model',
|
|
122
|
+
prompt: 'go',
|
|
123
|
+
workingDirectory: dir,
|
|
124
|
+
effort: 'xhigh',
|
|
125
|
+
thinking: { type: 'adaptive' },
|
|
126
|
+
timeoutMs: 30_000,
|
|
127
|
+
tokenBudget: 100_000,
|
|
128
|
+
maxIterations: 2,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
expect(provider.requests.length).toBeGreaterThan(0)
|
|
132
|
+
expect(provider.requests[0]?.effort, 'the front door dropped effort').toBe('xhigh')
|
|
133
|
+
expect(provider.requests[0]?.thinking?.type, 'the front door dropped thinking').toBe('adaptive')
|
|
134
|
+
})
|
|
135
|
+
})
|
|
@@ -177,6 +177,44 @@ describe('ToolExecutor — per-tool deadline', () => {
|
|
|
177
177
|
expect(batch.results[0]?.output).toContain('was cancelled')
|
|
178
178
|
})
|
|
179
179
|
|
|
180
|
+
it('says WHICH stop it was, when the caller named one', async () => {
|
|
181
|
+
// The reason was on this signal all along — it is forwarded into the
|
|
182
|
+
// per-tool controller a few lines above the message — and the message
|
|
183
|
+
// threw it away. So a deadline, a budget and an operator pressing stop
|
|
184
|
+
// all reached the model as the same four words, and they want
|
|
185
|
+
// different next moves: one is worth waiting out, one is worth
|
|
186
|
+
// narrowing the input for, and one is worth stopping over.
|
|
187
|
+
const controller = new AbortController()
|
|
188
|
+
const h = harness({
|
|
189
|
+
toolTimeoutMs: 60_000,
|
|
190
|
+
abortSignal: controller.signal,
|
|
191
|
+
run: never,
|
|
192
|
+
})
|
|
193
|
+
const pending = h.exec.executeBatch(response('hang'))
|
|
194
|
+
controller.abort(new Error('deployment window closed'))
|
|
195
|
+
const batch = await pending
|
|
196
|
+
|
|
197
|
+
expect(batch.results[0]?.output).toContain('deployment window closed')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('does not invent a reason when the caller gave none', async () => {
|
|
201
|
+
// `abort()` with no argument fills `reason` with a DOMException named
|
|
202
|
+
// AbortError. Rendering it would turn an honest silence into something
|
|
203
|
+
// that reads like an explanation, which is the worse failure.
|
|
204
|
+
const controller = new AbortController()
|
|
205
|
+
const h = harness({
|
|
206
|
+
toolTimeoutMs: 60_000,
|
|
207
|
+
abortSignal: controller.signal,
|
|
208
|
+
run: never,
|
|
209
|
+
})
|
|
210
|
+
const pending = h.exec.executeBatch(response('hang'))
|
|
211
|
+
controller.abort()
|
|
212
|
+
const batch = await pending
|
|
213
|
+
|
|
214
|
+
expect(batch.results[0]?.output).toContain('was cancelled.')
|
|
215
|
+
expect(batch.results[0]?.output).not.toContain('AbortError')
|
|
216
|
+
})
|
|
217
|
+
|
|
180
218
|
it('exposes a sane default deadline', () => {
|
|
181
219
|
// Documented so a change is a deliberate decision, not a drift.
|
|
182
220
|
expect(DEFAULT_TOOL_TIMEOUT_MS).toBe(120_000)
|
|
@@ -134,7 +134,22 @@ export class RunContextFactory {
|
|
|
134
134
|
static build(config: RunContextConfig): RunContext {
|
|
135
135
|
const abortController = new AbortController()
|
|
136
136
|
if (config.signal) {
|
|
137
|
-
|
|
137
|
+
// Forward the caller's REASON, not just the fact of the abort.
|
|
138
|
+
//
|
|
139
|
+
// This used to be a bare `abort()`. Every word a host attached to
|
|
140
|
+
// its stop — a deadline name, a budget, an operator's message —
|
|
141
|
+
// died one frame above the executor, so the most a tool result
|
|
142
|
+
// could say was "was cancelled". A run that ends for a nameable
|
|
143
|
+
// reason is a run someone can debug; this is the frame where the
|
|
144
|
+
// name was being thrown away.
|
|
145
|
+
//
|
|
146
|
+
// `createChildAbortController` already does exactly this, but it
|
|
147
|
+
// takes an AbortController and what arrives here is a bare
|
|
148
|
+
// AbortSignal, so the reason is forwarded by hand rather than by
|
|
149
|
+
// reaching for a helper that does not fit.
|
|
150
|
+
config.signal.addEventListener('abort', () => abortController.abort(config.signal?.reason), {
|
|
151
|
+
once: true,
|
|
152
|
+
})
|
|
138
153
|
}
|
|
139
154
|
|
|
140
155
|
const cwd = config.workingDirectory ?? process.cwd()
|
|
@@ -33,6 +33,7 @@ import type {
|
|
|
33
33
|
ToolCallRepair,
|
|
34
34
|
ToolCallRepairReason,
|
|
35
35
|
} from '../../types/tool/repair.js'
|
|
36
|
+
import { abortReasonText } from '../../utils/abort.js'
|
|
36
37
|
import { toErrorMessage } from '../../utils/error.js'
|
|
37
38
|
import type { Logger } from '../../utils/logger.js'
|
|
38
39
|
import { compressShellOutput } from '../../utils/shell-compress.js'
|
|
@@ -859,10 +860,19 @@ export class ToolExecutor {
|
|
|
859
860
|
}
|
|
860
861
|
|
|
861
862
|
if (outcome === 'aborted') {
|
|
863
|
+
// Say WHY, when the caller said why. The reason has been
|
|
864
|
+
// available on this signal all along — it is forwarded into
|
|
865
|
+
// `controller` a few lines above — and the message threw it
|
|
866
|
+
// away, so a deadline, a budget and an operator pressing stop
|
|
867
|
+
// were all reported to the model with the same four words.
|
|
868
|
+
// Those want different next moves.
|
|
869
|
+
const why = abortReasonText(controller.signal.reason)
|
|
862
870
|
return {
|
|
863
871
|
success: false,
|
|
864
872
|
output: '',
|
|
865
|
-
error:
|
|
873
|
+
error: why
|
|
874
|
+
? `Tool "${toolName}" was cancelled: ${why}`
|
|
875
|
+
: `Tool "${toolName}" was cancelled.`,
|
|
866
876
|
}
|
|
867
877
|
}
|
|
868
878
|
|
|
@@ -55,6 +55,38 @@ export class GuardCoordinator {
|
|
|
55
55
|
this.startTime = Date.now() - Math.max(0, elapsedMs)
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Wall-clock left before this run is asked to start finishing.
|
|
60
|
+
*
|
|
61
|
+
* NOT the time left before the deadline, and the difference is the whole
|
|
62
|
+
* point. The checks above run BETWEEN iterations, so anything that waits
|
|
63
|
+
* inside one cannot be stopped by them, and a caller sizing such a wait
|
|
64
|
+
* needs a number the run actually owns: a fixed two-minute hold measured
|
|
65
|
+
* against a run configured for twenty seconds kept it open for 120,267 ms.
|
|
66
|
+
*
|
|
67
|
+
* Measuring to the DEADLINE was the first attempt and it was wrong. The
|
|
68
|
+
* binding constraint is `budgetWarningThreshold`, the point at which this
|
|
69
|
+
* guard stops asking for more work and asks for a closing summary — that
|
|
70
|
+
* last slice exists so the run can produce an answer, and a wait sized
|
|
71
|
+
* against the deadline eats into it. Half of the time-to-deadline, started
|
|
72
|
+
* just under the threshold, ends at 95% of the budget: half the closing
|
|
73
|
+
* reserve spent waiting for a result the closing answer was supposed to
|
|
74
|
+
* use.
|
|
75
|
+
*
|
|
76
|
+
* Zero once the threshold has passed, which is also how a caller gets the
|
|
77
|
+
* re-evaluation it needs: `forceFinalize` is sampled at the top of an
|
|
78
|
+
* iteration and this is read when the wait is about to start, so a long
|
|
79
|
+
* iteration that crossed the line in between is told to wait for nothing.
|
|
80
|
+
*
|
|
81
|
+
* Reads through the same `startTime` the limit checks use, so a run
|
|
82
|
+
* resumed from a checkpoint (see `restoreElapsed`) reports the time left
|
|
83
|
+
* on the RUN rather than on the process now hosting it.
|
|
84
|
+
*/
|
|
85
|
+
remainingBeforeFinalizeMs(): number {
|
|
86
|
+
const finalizeAt = this.limitConfig.timeoutMs * this.limitConfig.budgetWarningThreshold
|
|
87
|
+
return Math.max(0, finalizeAt - (Date.now() - this.startTime))
|
|
88
|
+
}
|
|
89
|
+
|
|
58
90
|
beforeIteration(runMgr: RunPersistence, abortSignal: AbortSignal): GuardCheckResult {
|
|
59
91
|
const limitState = {
|
|
60
92
|
aborted: abortSignal.aborted,
|