@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,265 @@
|
|
|
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
|
+
|
|
6
|
+
import { CompletionInbox } from '../../../../gateway/completion-inbox.js'
|
|
7
|
+
import { ToolRegistry } from '../../../../registry/tool/execute.js'
|
|
8
|
+
import { DELEGATION_TIMEOUT_MS } from '../../../../tools/coordinator/index.js'
|
|
9
|
+
import type { TaskHandle } from '../../../../types/agent/gateway.js'
|
|
10
|
+
import type { SessionId, TaskId, TenantId } from '../../../../types/ids/index.js'
|
|
11
|
+
import { createUserMessage } from '../../../../types/message/index.js'
|
|
12
|
+
import type { LLMProvider, StreamChunk } from '../../../../types/provider/index.js'
|
|
13
|
+
import type { ProjectId, ThreadId } from '../../../../types/session/ids.js'
|
|
14
|
+
import { GuardCoordinator } from '../../guard.js'
|
|
15
|
+
import { drainQuery } from '../../index.js'
|
|
16
|
+
import { settleGraceMs } from '../index.js'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* How long a finishing run waits for a worker it launched.
|
|
20
|
+
*
|
|
21
|
+
* This used to be a constant — 120 seconds, unrelated to the run holding it.
|
|
22
|
+
* Measured before the change: a run configured `timeoutMs: 20_000` was held
|
|
23
|
+
* open for 120,267 ms, because the hold sits INSIDE an iteration and the guard
|
|
24
|
+
* only checks between them, so nothing could interrupt it. The same constant
|
|
25
|
+
* abandoned workers observed at 4m21s, 5m58s and 8m04s on runs that had hours
|
|
26
|
+
* left.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
describe('the grace is a share of what the run has left', () => {
|
|
30
|
+
it('takes half, so the turn that reads the result still has time to happen', () => {
|
|
31
|
+
// The fraction is the whole argument. Spending everything remaining
|
|
32
|
+
// would deliver a notification into a run with no turn left to act on
|
|
33
|
+
// it — the failure this mechanism exists to prevent, in a new costume.
|
|
34
|
+
expect(settleGraceMs(60_000)).toBe(30_000)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('holds for nothing when the run has nothing left', () => {
|
|
38
|
+
// A decision, not a rounding artefact: with no time left there is no
|
|
39
|
+
// turn in which to read a notification, so waiting can only delay a
|
|
40
|
+
// stop that is already due. Nothing is lost — `waitForArrival` returns
|
|
41
|
+
// before it looks at its timer when a completion is already in hand.
|
|
42
|
+
expect(settleGraceMs(0)).toBe(0)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('cannot outlive the deadline it was derived from', () => {
|
|
46
|
+
// Bounded by construction, which is why the guard's inability to
|
|
47
|
+
// interrupt a hold needs no new interrupt seam.
|
|
48
|
+
for (const remaining of [1, 250, 30_000, 600_000, 3_600_000]) {
|
|
49
|
+
expect(settleGraceMs(remaining)).toBeLessThan(remaining)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('stops at the longest this subsystem ever waits for a worker', () => {
|
|
54
|
+
// Only reachable for a host whose run timeout exceeds two hours.
|
|
55
|
+
expect(settleGraceMs(10 * 60 * 60 * 1000)).toBe(DELEGATION_TIMEOUT_MS)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('the guard reports time to the finalize point, not to the deadline', () => {
|
|
60
|
+
it('stops short of the closing reserve', () => {
|
|
61
|
+
// 90% of the budget, not 100%. The last tenth is what the guard keeps
|
|
62
|
+
// so a run can produce a closing answer; a wait sized against the
|
|
63
|
+
// deadline spends it on waiting instead.
|
|
64
|
+
const guard = new GuardCoordinator({ tokenBudget: 1_000, timeoutMs: 60_000 })
|
|
65
|
+
|
|
66
|
+
const remaining = guard.remainingBeforeFinalizeMs()
|
|
67
|
+
|
|
68
|
+
expect(remaining).toBeLessThanOrEqual(54_000)
|
|
69
|
+
expect(remaining).toBeGreaterThan(50_000)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('subtracts the time a previous process already spent', () => {
|
|
73
|
+
// A run resumed from a checkpoint gets the remainder of ITS budget, not
|
|
74
|
+
// a fresh clock — otherwise N resumes buy N x timeoutMs, and a hold
|
|
75
|
+
// sized from the fresh clock would outlive the real deadline.
|
|
76
|
+
const guard = new GuardCoordinator({ tokenBudget: 1_000, timeoutMs: 60_000 })
|
|
77
|
+
guard.restoreElapsed(50_000)
|
|
78
|
+
|
|
79
|
+
expect(guard.remainingBeforeFinalizeMs()).toBeLessThanOrEqual(4_000)
|
|
80
|
+
expect(guard.remainingBeforeFinalizeMs()).toBeGreaterThan(1_000)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('reports nothing once the run is already past the finalize point', () => {
|
|
84
|
+
// 95% elapsed: the guard is about to ask for a closing summary, and a
|
|
85
|
+
// hold opened here would be taken out of the answer's time.
|
|
86
|
+
const guard = new GuardCoordinator({ tokenBudget: 1_000, timeoutMs: 60_000 })
|
|
87
|
+
guard.restoreElapsed(57_000)
|
|
88
|
+
|
|
89
|
+
expect(guard.remainingBeforeFinalizeMs()).toBe(0)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('never reports a negative remainder', () => {
|
|
93
|
+
const guard = new GuardCoordinator({ tokenBudget: 1_000, timeoutMs: 1_000 })
|
|
94
|
+
guard.restoreElapsed(500_000)
|
|
95
|
+
|
|
96
|
+
expect(guard.remainingBeforeFinalizeMs()).toBe(0)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('the hold cannot reach into the run closing reserve', () => {
|
|
101
|
+
it('ends before the finalize point however late it starts', () => {
|
|
102
|
+
// The failure the finalize-relative input exists for. Against the
|
|
103
|
+
// DEADLINE, a hold beginning at elapsed fraction e ends at
|
|
104
|
+
// 0.5 + 0.5e — so one starting just under the 0.9 threshold ends at
|
|
105
|
+
// 95% of the budget, with half the closing reserve gone. Against the
|
|
106
|
+
// finalize point it cannot cross 90% at all.
|
|
107
|
+
const timeoutMs = 60_000
|
|
108
|
+
const finalizeAt = timeoutMs * 0.9
|
|
109
|
+
|
|
110
|
+
for (const elapsed of [0, 30_000, 50_000, 53_000, 53_900]) {
|
|
111
|
+
const remainingBeforeFinalize = Math.max(0, finalizeAt - elapsed)
|
|
112
|
+
const endsAt = elapsed + settleGraceMs(remainingBeforeFinalize)
|
|
113
|
+
|
|
114
|
+
expect(
|
|
115
|
+
endsAt,
|
|
116
|
+
`a hold starting at ${elapsed}ms crossed the finalize point`,
|
|
117
|
+
).toBeLessThanOrEqual(finalizeAt)
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const ZERO_USAGE = {
|
|
123
|
+
promptTokens: 0,
|
|
124
|
+
completionTokens: 0,
|
|
125
|
+
totalTokens: 0,
|
|
126
|
+
cachedTokens: 0,
|
|
127
|
+
cacheWriteTokens: 0,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Answers straight away. The hold is the only thing that can delay this run. */
|
|
131
|
+
class AnswersImmediately implements LLMProvider {
|
|
132
|
+
readonly id = 'answers'
|
|
133
|
+
readonly name = 'Answers Immediately'
|
|
134
|
+
async *chatStream(): AsyncIterable<StreamChunk> {
|
|
135
|
+
yield { id: 'm1', delta: { content: 'Done.' } }
|
|
136
|
+
yield { id: 'm1', delta: {}, finishReason: 'stop', usage: ZERO_USAGE }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const workdirs: string[] = []
|
|
141
|
+
afterEach(async () => {
|
|
142
|
+
await Promise.all(workdirs.map((dir) => rm(dir, { recursive: true, force: true })))
|
|
143
|
+
workdirs.length = 0
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The loop, not the helper.
|
|
148
|
+
*
|
|
149
|
+
* `settleGraceMs` has its own tests above and every one of them passes with
|
|
150
|
+
* the loop still calling a constant. Only a real run can say which number the
|
|
151
|
+
* hold actually used.
|
|
152
|
+
*/
|
|
153
|
+
describe('the hold a run pays is the one its own budget allows', () => {
|
|
154
|
+
async function runHoldingFor(timeoutMs: number): Promise<number> {
|
|
155
|
+
const workingDirectory = await mkdtemp(join(tmpdir(), 'namzu-grace-'))
|
|
156
|
+
workdirs.push(workingDirectory)
|
|
157
|
+
|
|
158
|
+
const inbox = new CompletionInbox()
|
|
159
|
+
// `getTask` because the inbox asks the gateway about a task whose
|
|
160
|
+
// completion may have been announced before the launch was recorded.
|
|
161
|
+
// A double that omits a method the interface requires is not a smaller
|
|
162
|
+
// gateway, it is a broken one.
|
|
163
|
+
inbox.attach({ onTaskCompleted: () => () => {}, getTask: () => undefined } as never)
|
|
164
|
+
// Outstanding and never settling: the worst case the bound exists for.
|
|
165
|
+
// `expect` records the launch as well, so the inbox owns this task.
|
|
166
|
+
inbox.expect('tsk_never' as TaskId)
|
|
167
|
+
|
|
168
|
+
const startedAt = Date.now()
|
|
169
|
+
await drainQuery({
|
|
170
|
+
provider: new AnswersImmediately(),
|
|
171
|
+
tools: new ToolRegistry(),
|
|
172
|
+
completionInbox: inbox,
|
|
173
|
+
agentId: 'agent_test',
|
|
174
|
+
agentName: 'Test Agent',
|
|
175
|
+
messages: [createUserMessage('go')],
|
|
176
|
+
workingDirectory,
|
|
177
|
+
runConfig: {
|
|
178
|
+
model: 'mock-model',
|
|
179
|
+
timeoutMs,
|
|
180
|
+
tokenBudget: 100_000,
|
|
181
|
+
maxIterations: 2,
|
|
182
|
+
maxResponseTokens: 256,
|
|
183
|
+
},
|
|
184
|
+
sessionId: 'ses_grace' as SessionId,
|
|
185
|
+
threadId: 'thd_grace' as ThreadId,
|
|
186
|
+
projectId: 'prj_grace' as ProjectId,
|
|
187
|
+
tenantId: 'tnt_grace' as TenantId,
|
|
188
|
+
} as never)
|
|
189
|
+
return Date.now() - startedAt
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
it('a two-second run does not wait two minutes for a worker', async () => {
|
|
193
|
+
const elapsed = await runHoldingFor(2_000)
|
|
194
|
+
|
|
195
|
+
// Half of two seconds, plus whatever the turn itself costs. The number
|
|
196
|
+
// this replaced would have parked here for 120 s regardless.
|
|
197
|
+
expect(elapsed, 'the hold outlived the run budget that bounds it').toBeLessThan(5_000)
|
|
198
|
+
}, 200_000)
|
|
199
|
+
|
|
200
|
+
it('a run with no time left does not hold at all', async () => {
|
|
201
|
+
// The guard stops this run at the top of its first iteration, so no
|
|
202
|
+
// hold is even reached — which is the point: the last tenth of a run
|
|
203
|
+
// already never holds, so no artificial minimum has to defend it.
|
|
204
|
+
const elapsed = await runHoldingFor(1)
|
|
205
|
+
|
|
206
|
+
expect(elapsed).toBeLessThan(2_000)
|
|
207
|
+
}, 200_000)
|
|
208
|
+
|
|
209
|
+
it('still delivers a completion that arrives inside the grace', async () => {
|
|
210
|
+
// The bound must not become an excuse to drop what was nearly ready.
|
|
211
|
+
const workingDirectory = await mkdtemp(join(tmpdir(), 'namzu-grace-hit-'))
|
|
212
|
+
workdirs.push(workingDirectory)
|
|
213
|
+
|
|
214
|
+
const inbox = new CompletionInbox()
|
|
215
|
+
let announce: ((h: TaskHandle) => void) | undefined
|
|
216
|
+
inbox.attach({
|
|
217
|
+
onTaskCompleted: (cb: (h: TaskHandle) => void) => {
|
|
218
|
+
announce = cb
|
|
219
|
+
return () => {
|
|
220
|
+
announce = undefined
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
getTask: () => undefined,
|
|
224
|
+
} as never)
|
|
225
|
+
inbox.expect('tsk_soon' as TaskId)
|
|
226
|
+
const timer = setTimeout(() => {
|
|
227
|
+
announce?.({
|
|
228
|
+
taskId: 'tsk_soon' as TaskId,
|
|
229
|
+
agentId: 'reviewer',
|
|
230
|
+
state: 'completed',
|
|
231
|
+
createdAt: 0,
|
|
232
|
+
completedAt: 1,
|
|
233
|
+
result: { status: 'completed', result: 'ARRIVED INSIDE THE GRACE' },
|
|
234
|
+
} as TaskHandle)
|
|
235
|
+
}, 150)
|
|
236
|
+
timer.unref?.()
|
|
237
|
+
|
|
238
|
+
const run = await drainQuery({
|
|
239
|
+
provider: new AnswersImmediately(),
|
|
240
|
+
tools: new ToolRegistry(),
|
|
241
|
+
completionInbox: inbox,
|
|
242
|
+
agentId: 'agent_test',
|
|
243
|
+
agentName: 'Test Agent',
|
|
244
|
+
messages: [createUserMessage('go')],
|
|
245
|
+
workingDirectory,
|
|
246
|
+
runConfig: {
|
|
247
|
+
model: 'mock-model',
|
|
248
|
+
timeoutMs: 30_000,
|
|
249
|
+
tokenBudget: 100_000,
|
|
250
|
+
maxIterations: 3,
|
|
251
|
+
maxResponseTokens: 256,
|
|
252
|
+
},
|
|
253
|
+
sessionId: 'ses_grace' as SessionId,
|
|
254
|
+
threadId: 'thd_grace' as ThreadId,
|
|
255
|
+
projectId: 'prj_grace' as ProjectId,
|
|
256
|
+
tenantId: 'tnt_grace' as TenantId,
|
|
257
|
+
} as never)
|
|
258
|
+
|
|
259
|
+
const userText = (run.messages as { role: string; content: unknown }[])
|
|
260
|
+
.filter((m) => m.role === 'user')
|
|
261
|
+
.map((m) => (typeof m.content === 'string' ? m.content : ''))
|
|
262
|
+
|
|
263
|
+
expect(userText.some((m) => m.includes('ARRIVED INSIDE THE GRACE'))).toBe(true)
|
|
264
|
+
}, 200_000)
|
|
265
|
+
})
|