@namzu/sdk 1.0.0 → 1.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 +253 -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 +22 -1
- package/dist/agents/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/SupervisorAgent.js +47 -14
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/agents/__tests__/supervisor-ledger.test.d.ts +2 -0
- package/dist/agents/__tests__/supervisor-ledger.test.d.ts.map +1 -0
- package/dist/agents/__tests__/supervisor-ledger.test.js +131 -0
- package/dist/agents/__tests__/supervisor-ledger.test.js.map +1 -0
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +2 -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 +4 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/config/runtime.d.ts +24 -0
- package/dist/config/runtime.d.ts.map +1 -1
- package/dist/config/runtime.js +9 -0
- package/dist/config/runtime.js.map +1 -1
- package/dist/connector/builtins/webhook.test.js +17 -0
- package/dist/connector/builtins/webhook.test.js.map +1 -1
- package/dist/connector/index.d.ts +1 -0
- package/dist/connector/index.d.ts.map +1 -1
- package/dist/connector/index.js +1 -0
- package/dist/connector/index.js.map +1 -1
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +4 -0
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/connector/mcp/index.d.ts +1 -0
- package/dist/connector/mcp/index.d.ts.map +1 -1
- package/dist/connector/mcp/index.js +1 -0
- package/dist/connector/mcp/index.js.map +1 -1
- package/dist/connector/mcp/streamable-http.d.ts +22 -0
- package/dist/connector/mcp/streamable-http.d.ts.map +1 -0
- package/dist/connector/mcp/streamable-http.js +128 -0
- package/dist/connector/mcp/streamable-http.js.map +1 -0
- package/dist/connector/mcp/streamable-http.test.d.ts +2 -0
- package/dist/connector/mcp/streamable-http.test.d.ts.map +1 -0
- package/dist/connector/mcp/streamable-http.test.js +132 -0
- package/dist/connector/mcp/streamable-http.test.js.map +1 -0
- package/dist/{runtime/query → constants}/continuation.d.ts +5 -0
- package/dist/constants/continuation.d.ts.map +1 -0
- package/dist/{runtime/query → constants}/continuation.js +5 -0
- package/dist/constants/continuation.js.map +1 -0
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.d.ts.map +1 -1
- package/dist/constants/index.js +1 -0
- package/dist/constants/index.js.map +1 -1
- package/dist/manager/run/persistence.d.ts +11 -0
- package/dist/manager/run/persistence.d.ts.map +1 -1
- package/dist/manager/run/persistence.js +30 -1
- package/dist/manager/run/persistence.js.map +1 -1
- package/dist/persona/assembler.d.ts.map +1 -1
- package/dist/persona/assembler.js +18 -9
- package/dist/persona/assembler.js.map +1 -1
- package/dist/provider/capabilities.d.ts +27 -0
- package/dist/provider/capabilities.d.ts.map +1 -0
- package/dist/provider/capabilities.js +29 -0
- package/dist/provider/capabilities.js.map +1 -0
- package/dist/provider/collect.d.ts.map +1 -1
- package/dist/provider/collect.js +4 -1
- package/dist/provider/collect.js.map +1 -1
- package/dist/provider/index.d.ts +2 -0
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +1 -0
- package/dist/provider/index.js.map +1 -1
- package/dist/public-runtime.d.ts +4 -3
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +3 -3
- package/dist/public-runtime.js.map +1 -1
- package/dist/registry/tool/execute.d.ts +27 -0
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +124 -21
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/registry/tool/execute.test.d.ts +9 -6
- package/dist/registry/tool/execute.test.d.ts.map +1 -1
- package/dist/registry/tool/execute.test.js +72 -10
- package/dist/registry/tool/execute.test.js.map +1 -1
- package/dist/registry/toolset/catalog.test.js +62 -0
- package/dist/registry/toolset/catalog.test.js.map +1 -1
- package/dist/run/reporter.d.ts.map +1 -1
- package/dist/run/reporter.js +8 -0
- package/dist/run/reporter.js.map +1 -1
- package/dist/runtime/query/__tests__/capability-negotiation.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/capability-negotiation.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/capability-negotiation.test.js +184 -0
- package/dist/runtime/query/__tests__/capability-negotiation.test.js.map +1 -0
- package/dist/runtime/query/__tests__/checkpoint-store.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/checkpoint-store.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/checkpoint-store.test.js +259 -0
- package/dist/runtime/query/__tests__/checkpoint-store.test.js.map +1 -0
- package/dist/runtime/query/__tests__/checkpoint.test.js +9 -3
- package/dist/runtime/query/__tests__/checkpoint.test.js.map +1 -1
- package/dist/runtime/query/__tests__/deferred-tools.test.js +43 -5
- package/dist/runtime/query/__tests__/deferred-tools.test.js.map +1 -1
- package/dist/runtime/query/__tests__/long-document-flow.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/long-document-flow.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/long-document-flow.test.js +154 -0
- package/dist/runtime/query/__tests__/long-document-flow.test.js.map +1 -0
- package/dist/runtime/query/__tests__/orphan-task-turn-end.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/orphan-task-turn-end.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/orphan-task-turn-end.test.js +110 -0
- package/dist/runtime/query/__tests__/orphan-task-turn-end.test.js.map +1 -0
- package/dist/runtime/query/__tests__/prompt.test.js +15 -11
- package/dist/runtime/query/__tests__/prompt.test.js.map +1 -1
- package/dist/runtime/query/checkpoint.d.ts +16 -2
- package/dist/runtime/query/checkpoint.d.ts.map +1 -1
- package/dist/runtime/query/checkpoint.js +21 -7
- package/dist/runtime/query/checkpoint.js.map +1 -1
- package/dist/runtime/query/context.d.ts +7 -0
- package/dist/runtime/query/context.d.ts.map +1 -1
- package/dist/runtime/query/context.js +1 -0
- package/dist/runtime/query/context.js.map +1 -1
- package/dist/runtime/query/index.d.ts +29 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +98 -8
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +3 -55
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +260 -656
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/checkpoint.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/checkpoint.js +24 -2
- package/dist/runtime/query/iteration/phases/checkpoint.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts +8 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +84 -3
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +13 -2
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js +53 -0
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/hitl-abort-race.test.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/hitl-abort-race.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/hitl-abort-race.test.js +73 -0
- package/dist/runtime/query/iteration/phases/hitl-abort-race.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/hitl-answer-question.test.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/hitl-answer-question.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/hitl-answer-question.test.js +120 -0
- package/dist/runtime/query/iteration/phases/hitl-answer-question.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/tool-review.d.ts +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +7 -2
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/iteration/phases/working-memory-compaction.test.d.ts +19 -0
- package/dist/runtime/query/iteration/phases/working-memory-compaction.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/working-memory-compaction.test.js +177 -0
- package/dist/runtime/query/iteration/phases/working-memory-compaction.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/working-memory.d.ts +31 -0
- package/dist/runtime/query/iteration/phases/working-memory.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/working-memory.js +81 -0
- package/dist/runtime/query/iteration/phases/working-memory.js.map +1 -0
- package/dist/runtime/query/iteration/stream-turn.d.ts +35 -0
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -0
- package/dist/runtime/query/iteration/stream-turn.js +341 -0
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -0
- package/dist/runtime/query/replay/list.d.ts +17 -1
- package/dist/runtime/query/replay/list.d.ts.map +1 -1
- package/dist/runtime/query/replay/list.js +21 -4
- package/dist/runtime/query/replay/list.js.map +1 -1
- package/dist/runtime/query/replay/prepare.d.ts +10 -0
- package/dist/runtime/query/replay/prepare.d.ts.map +1 -1
- package/dist/runtime/query/replay/prepare.js +20 -4
- package/dist/runtime/query/replay/prepare.js.map +1 -1
- package/dist/session/workspace/__tests__/shared-run.test.js +1 -1
- package/dist/session/workspace/__tests__/shared-run.test.js.map +1 -1
- package/dist/session/workspace/shared-run.js +1 -0
- package/dist/session/workspace/shared-run.js.map +1 -1
- package/dist/skills/loader.test.js +8 -8
- package/dist/skills/loader.test.js.map +1 -1
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +1 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/run/checkpoint-disk.d.ts +28 -0
- package/dist/store/run/checkpoint-disk.d.ts.map +1 -0
- package/dist/store/run/checkpoint-disk.js +55 -0
- package/dist/store/run/checkpoint-disk.js.map +1 -0
- package/dist/streaming/coalesce.test.js.map +1 -1
- package/dist/tools/builtins/__tests__/payload-budget.test.js +48 -0
- package/dist/tools/builtins/__tests__/payload-budget.test.js.map +1 -1
- package/dist/tools/builtins/__tests__/read-file.test.js +9 -0
- package/dist/tools/builtins/__tests__/read-file.test.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +50 -2
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/search-tools.d.ts.map +1 -1
- package/dist/tools/builtins/search-tools.js +32 -7
- package/dist/tools/builtins/search-tools.js.map +1 -1
- package/dist/tools/coordinator/__tests__/approve-plan.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/approve-plan.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/approve-plan.test.js +114 -0
- package/dist/tools/coordinator/__tests__/approve-plan.test.js.map +1 -0
- package/dist/tools/coordinator/__tests__/ask-user-question.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/ask-user-question.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/ask-user-question.test.js +309 -0
- package/dist/tools/coordinator/__tests__/ask-user-question.test.js.map +1 -0
- package/dist/tools/coordinator/index.d.ts +8 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +195 -15
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/types/agent/index.d.ts +1 -0
- package/dist/types/agent/index.d.ts.map +1 -1
- package/dist/types/agent/index.js +1 -0
- package/dist/types/agent/index.js.map +1 -1
- package/dist/types/agent/reactive.d.ts +15 -0
- package/dist/types/agent/reactive.d.ts.map +1 -1
- package/dist/types/agent/supervisor.d.ts +18 -0
- package/dist/types/agent/supervisor.d.ts.map +1 -1
- package/dist/types/agent/working-memory.d.ts +29 -0
- package/dist/types/agent/working-memory.d.ts.map +1 -0
- package/dist/types/agent/working-memory.js +2 -0
- package/dist/types/agent/working-memory.js.map +1 -0
- package/dist/types/common/index.d.ts +10 -0
- package/dist/types/common/index.d.ts.map +1 -1
- package/dist/types/common/index.js +18 -0
- package/dist/types/common/index.js.map +1 -1
- package/dist/types/common/usage-merge.test.d.ts +2 -0
- package/dist/types/common/usage-merge.test.d.ts.map +1 -0
- package/dist/types/common/usage-merge.test.js +48 -0
- package/dist/types/common/usage-merge.test.js.map +1 -0
- package/dist/types/connector/mcp.d.ts +9 -2
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +42 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js +11 -0
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/provider/chat.d.ts +9 -0
- package/dist/types/provider/chat.d.ts.map +1 -1
- package/dist/types/provider/config.d.ts +18 -0
- package/dist/types/provider/config.d.ts.map +1 -1
- package/dist/types/provider/interface.d.ts +10 -0
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/run/checkpoint-store.d.ts +64 -0
- package/dist/types/run/checkpoint-store.d.ts.map +1 -0
- package/dist/types/run/checkpoint-store.js +12 -0
- package/dist/types/run/checkpoint-store.js.map +1 -0
- package/dist/types/run/config.d.ts +27 -0
- package/dist/types/run/config.d.ts.map +1 -1
- package/dist/types/run/events.d.ts +6 -0
- package/dist/types/run/events.d.ts.map +1 -1
- package/dist/types/run/events.js.map +1 -1
- package/dist/types/run/index.d.ts +1 -0
- package/dist/types/run/index.d.ts.map +1 -1
- package/dist/types/run/index.js +1 -0
- package/dist/types/run/index.js.map +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +25 -4
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/ReactiveAgent.ts +5 -0
- package/src/agents/SupervisorAgent.ts +56 -16
- package/src/agents/__tests__/supervisor-ledger.test.ts +185 -0
- package/src/bridge/a2a/mapper.ts +3 -0
- package/src/bridge/sse/mapper.ts +5 -0
- package/src/config/runtime.ts +9 -0
- package/src/connector/builtins/webhook.test.ts +21 -0
- package/src/connector/index.ts +1 -0
- package/src/connector/mcp/client.ts +4 -0
- package/src/connector/mcp/index.ts +1 -0
- package/src/connector/mcp/streamable-http.test.ts +169 -0
- package/src/connector/mcp/streamable-http.ts +152 -0
- package/src/{runtime/query → constants}/continuation.ts +5 -0
- package/src/constants/index.ts +1 -0
- package/src/manager/run/persistence.ts +34 -1
- package/src/persona/assembler.ts +18 -6
- package/src/provider/capabilities.ts +47 -0
- package/src/provider/collect.ts +4 -1
- package/src/provider/index.ts +5 -0
- package/src/public-runtime.ts +5 -0
- package/src/registry/tool/execute.test.ts +85 -10
- package/src/registry/tool/execute.ts +124 -21
- package/src/registry/toolset/catalog.test.ts +68 -0
- package/src/run/reporter.ts +9 -0
- package/src/runtime/query/__tests__/capability-negotiation.test.ts +248 -0
- package/src/runtime/query/__tests__/checkpoint-store.test.ts +322 -0
- package/src/runtime/query/__tests__/checkpoint.test.ts +15 -6
- package/src/runtime/query/__tests__/deferred-tools.test.ts +52 -5
- package/src/runtime/query/__tests__/long-document-flow.test.ts +180 -0
- package/src/runtime/query/__tests__/orphan-task-turn-end.test.ts +127 -0
- package/src/runtime/query/__tests__/prompt.test.ts +15 -11
- package/src/runtime/query/checkpoint.ts +23 -9
- package/src/runtime/query/context.ts +9 -0
- package/src/runtime/query/index.ts +148 -29
- package/src/runtime/query/iteration/index.ts +316 -831
- package/src/runtime/query/iteration/phases/checkpoint.ts +32 -2
- package/src/runtime/query/iteration/phases/compaction.ts +89 -3
- package/src/runtime/query/iteration/phases/context.ts +59 -3
- package/src/runtime/query/iteration/phases/hitl-abort-race.test.ts +86 -0
- package/src/runtime/query/iteration/phases/hitl-answer-question.test.ts +139 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +7 -3
- package/src/runtime/query/iteration/phases/working-memory-compaction.test.ts +215 -0
- package/src/runtime/query/iteration/phases/working-memory.ts +86 -0
- package/src/runtime/query/iteration/stream-turn.ts +396 -0
- package/src/runtime/query/replay/list.ts +38 -4
- package/src/runtime/query/replay/prepare.ts +37 -4
- package/src/session/workspace/__tests__/shared-run.test.ts +1 -1
- package/src/session/workspace/shared-run.ts +1 -0
- package/src/skills/loader.test.ts +8 -8
- package/src/store/index.ts +1 -0
- package/src/store/run/checkpoint-disk.ts +67 -0
- package/src/streaming/coalesce.test.ts +3 -3
- package/src/tools/builtins/__tests__/payload-budget.test.ts +63 -0
- package/src/tools/builtins/__tests__/read-file.test.ts +12 -0
- package/src/tools/builtins/read-file.ts +50 -2
- package/src/tools/builtins/search-tools.ts +38 -7
- package/src/tools/coordinator/__tests__/approve-plan.test.ts +139 -0
- package/src/tools/coordinator/__tests__/ask-user-question.test.ts +370 -0
- package/src/tools/coordinator/index.ts +227 -17
- package/src/types/agent/index.ts +1 -0
- package/src/types/agent/reactive.ts +17 -0
- package/src/types/agent/supervisor.ts +20 -0
- package/src/types/agent/working-memory.ts +29 -0
- package/src/types/common/index.ts +19 -0
- package/src/types/common/usage-merge.test.ts +53 -0
- package/src/types/connector/mcp.ts +14 -2
- package/src/types/hitl/index.ts +52 -1
- package/src/types/provider/chat.ts +10 -0
- package/src/types/provider/config.ts +18 -0
- package/src/types/provider/interface.ts +11 -0
- package/src/types/run/checkpoint-store.ts +72 -0
- package/src/types/run/config.ts +30 -0
- package/src/types/run/events.ts +13 -0
- package/src/types/run/index.ts +1 -0
- package/src/version.ts +26 -4
- package/dist/runtime/query/continuation.d.ts.map +0 -1
- package/dist/runtime/query/continuation.js.map +0 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for the ses_055 Layer-B SDK seam (#104):
|
|
3
|
+
*
|
|
4
|
+
* 1. **Compaction FIRES** under a small `contextWindowTokens` even with the
|
|
5
|
+
* run-level `tokenBudget = 0` (UNLIMITED) — proving the F1 fix repointed
|
|
6
|
+
* BOTH the `<= 0` guard AND the divisor, closing the silent-no-op trap.
|
|
7
|
+
* 2. **The pinned working-memory slot SURVIVES** the compaction pass (it is a
|
|
8
|
+
* leading system message → preserved by header identity).
|
|
9
|
+
* 3. **No premature stop** from `tokenBudget = 0` alone: with NO
|
|
10
|
+
* `contextWindowTokens`, the same overflowing run does NOT compact (the
|
|
11
|
+
* legacy fallback), so consumers that never set the new field are
|
|
12
|
+
* byte-identical.
|
|
13
|
+
*
|
|
14
|
+
* The test drives `refreshWorkingMemory` (to pin the slot) then
|
|
15
|
+
* `runCompactionCheck` directly against a mock `IterationContext`, which is the
|
|
16
|
+
* exact sequence the iteration loop runs each turn.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
20
|
+
|
|
21
|
+
import { findDanglingMessages } from '../../../../compaction/dangling.js'
|
|
22
|
+
import { WorkingStateManager } from '../../../../compaction/manager.js'
|
|
23
|
+
import { CompactionConfigSchema } from '../../../../config/runtime.js'
|
|
24
|
+
import type { RunId } from '../../../../types/ids/index.js'
|
|
25
|
+
import {
|
|
26
|
+
type Message,
|
|
27
|
+
createAssistantMessage,
|
|
28
|
+
createSystemMessage,
|
|
29
|
+
createToolMessage,
|
|
30
|
+
createUserMessage,
|
|
31
|
+
} from '../../../../types/message/index.js'
|
|
32
|
+
import type { Logger } from '../../../../utils/logger.js'
|
|
33
|
+
import { runCompactionCheck } from './compaction.js'
|
|
34
|
+
import type { IterationContext } from './context.js'
|
|
35
|
+
import { WORKING_MEMORY_HEADER, refreshWorkingMemory } from './working-memory.js'
|
|
36
|
+
|
|
37
|
+
function makeLogger(): Logger {
|
|
38
|
+
const self = {
|
|
39
|
+
info: vi.fn(),
|
|
40
|
+
warn: vi.fn(),
|
|
41
|
+
error: vi.fn(),
|
|
42
|
+
debug: vi.fn(),
|
|
43
|
+
child: vi.fn(),
|
|
44
|
+
} as unknown as Logger
|
|
45
|
+
;(self as { child: (ctx: unknown) => Logger }).child = vi.fn(() => self)
|
|
46
|
+
return self
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A long string so a handful of messages overflow a small context window. */
|
|
50
|
+
const FILLER = 'x'.repeat(200)
|
|
51
|
+
|
|
52
|
+
function buildOverflowingMessages(): Message[] {
|
|
53
|
+
const msgs: Message[] = [createSystemMessage(`STATIC SYSTEM PROMPT ${FILLER}`, 'cache')]
|
|
54
|
+
for (let i = 0; i < 8; i++) {
|
|
55
|
+
msgs.push(createUserMessage(`user turn ${i} ${FILLER}`))
|
|
56
|
+
msgs.push(createAssistantMessage(`assistant turn ${i} ${FILLER}`))
|
|
57
|
+
}
|
|
58
|
+
return msgs
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function makeCtx(opts: {
|
|
62
|
+
messages: Message[]
|
|
63
|
+
contextWindowTokens?: number
|
|
64
|
+
workingMemoryProvider?: IterationContext['workingMemoryProvider']
|
|
65
|
+
}): IterationContext {
|
|
66
|
+
const config = CompactionConfigSchema.parse({
|
|
67
|
+
strategy: 'structured',
|
|
68
|
+
llmVerification: false,
|
|
69
|
+
...(opts.contextWindowTokens !== undefined
|
|
70
|
+
? { contextWindowTokens: opts.contextWindowTokens }
|
|
71
|
+
: {}),
|
|
72
|
+
})
|
|
73
|
+
const manager = new WorkingStateManager(config)
|
|
74
|
+
// Seed a couple of slots so serializeState produces non-trivial output.
|
|
75
|
+
manager.addDecision('built the report as .docx')
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
runConfig: { tokenBudget: 0 }, // UNLIMITED cumulative cost cap
|
|
79
|
+
compactionConfig: config,
|
|
80
|
+
workingStateManager: manager,
|
|
81
|
+
workingMemoryProvider: opts.workingMemoryProvider,
|
|
82
|
+
log: makeLogger(),
|
|
83
|
+
runMgr: {
|
|
84
|
+
id: 'run_1' as RunId,
|
|
85
|
+
currentIteration: 3,
|
|
86
|
+
messages: opts.messages,
|
|
87
|
+
},
|
|
88
|
+
} as unknown as IterationContext
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const WM_BLOCK =
|
|
92
|
+
'ARTIFACTS\n • report.docx · Word document · 48.2 KB · imp:H\n path: /mnt/user-data/outputs/report.docx'
|
|
93
|
+
|
|
94
|
+
describe('Layer-B compaction seam (ses_055 #104)', () => {
|
|
95
|
+
it('FIRES on small contextWindowTokens with tokenBudget=0 AND preserves the pinned WM slot', async () => {
|
|
96
|
+
const messages = buildOverflowingMessages()
|
|
97
|
+
const ctx = makeCtx({
|
|
98
|
+
messages,
|
|
99
|
+
contextWindowTokens: 100,
|
|
100
|
+
workingMemoryProvider: () => WM_BLOCK,
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
await refreshWorkingMemory(ctx)
|
|
104
|
+
// The slot is pinned as the last leading system message (index 1).
|
|
105
|
+
expect(messages[1]?.role).toBe('system')
|
|
106
|
+
expect(messages[1]?.content?.startsWith(WORKING_MEMORY_HEADER)).toBe(true)
|
|
107
|
+
|
|
108
|
+
const before = messages.length
|
|
109
|
+
await runCompactionCheck(ctx)
|
|
110
|
+
|
|
111
|
+
// (1) compaction fired: a [COMPACTED CONTEXT] message appeared and the
|
|
112
|
+
// transcript shrank.
|
|
113
|
+
expect(messages.length).toBeLessThan(before)
|
|
114
|
+
expect(messages.some((m) => m.content?.includes('[COMPACTED CONTEXT]'))).toBe(true)
|
|
115
|
+
|
|
116
|
+
// (2) the pinned working-memory slot survived by header identity.
|
|
117
|
+
const wm = messages.filter(
|
|
118
|
+
(m) => m.role === 'system' && m.content?.startsWith(WORKING_MEMORY_HEADER),
|
|
119
|
+
)
|
|
120
|
+
expect(wm).toHaveLength(1)
|
|
121
|
+
expect(wm[0]?.content).toContain('report.docx')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('does NOT compact when contextWindowTokens is absent and tokenBudget=0 (byte-identical fallback)', async () => {
|
|
125
|
+
const messages = buildOverflowingMessages()
|
|
126
|
+
const ctx = makeCtx({ messages })
|
|
127
|
+
|
|
128
|
+
const before = messages.length
|
|
129
|
+
await runCompactionCheck(ctx)
|
|
130
|
+
|
|
131
|
+
// budget = tokenBudget = 0 → guard returns; no compaction, no mutation.
|
|
132
|
+
expect(messages.length).toBe(before)
|
|
133
|
+
expect(messages.some((m) => m.content?.includes('[COMPACTED CONTEXT]'))).toBe(false)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('removes the slot when the provider returns an empty block', async () => {
|
|
137
|
+
const messages = buildOverflowingMessages()
|
|
138
|
+
const ctx = makeCtx({
|
|
139
|
+
messages,
|
|
140
|
+
contextWindowTokens: 100,
|
|
141
|
+
workingMemoryProvider: () => WM_BLOCK,
|
|
142
|
+
})
|
|
143
|
+
await refreshWorkingMemory(ctx)
|
|
144
|
+
expect(messages.some((m) => m.content?.startsWith(WORKING_MEMORY_HEADER))).toBe(true)
|
|
145
|
+
|
|
146
|
+
// Same transcript, provider now returns blank ⇒ the slot is removed.
|
|
147
|
+
const blankCtx = makeCtx({
|
|
148
|
+
messages,
|
|
149
|
+
contextWindowTokens: 100,
|
|
150
|
+
workingMemoryProvider: () => ' ',
|
|
151
|
+
})
|
|
152
|
+
await refreshWorkingMemory(blankCtx)
|
|
153
|
+
expect(messages.some((m) => m.content?.startsWith(WORKING_MEMORY_HEADER))).toBe(false)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('keeps the prior slot when the provider throws (failure-isolated)', async () => {
|
|
157
|
+
const messages = buildOverflowingMessages()
|
|
158
|
+
const ctx = makeCtx({
|
|
159
|
+
messages,
|
|
160
|
+
contextWindowTokens: 100,
|
|
161
|
+
workingMemoryProvider: () => WM_BLOCK,
|
|
162
|
+
})
|
|
163
|
+
await refreshWorkingMemory(ctx)
|
|
164
|
+
|
|
165
|
+
const throwingCtx = makeCtx({
|
|
166
|
+
messages,
|
|
167
|
+
contextWindowTokens: 100,
|
|
168
|
+
workingMemoryProvider: () => {
|
|
169
|
+
throw new Error('provider boom')
|
|
170
|
+
},
|
|
171
|
+
})
|
|
172
|
+
await refreshWorkingMemory(throwingCtx)
|
|
173
|
+
// Prior slot retained; run not broken.
|
|
174
|
+
expect(messages.some((m) => m.content?.startsWith(WORKING_MEMORY_HEADER))).toBe(true)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('does NOT orphan a tool_result when the recent-window cut splits a tool pair (C1)', async () => {
|
|
178
|
+
// keepRecentMessages defaults to 4. Lay the transcript out so the NAIVE
|
|
179
|
+
// cut (length - 4 = index 5) lands right AFTER the assistant-with-toolCall
|
|
180
|
+
// (index 4) but BEFORE... no — exactly ON its tool result (index 5), which
|
|
181
|
+
// a naive slice would keep in the recent window while dropping the
|
|
182
|
+
// assistant into the summarized older set → an orphaned tool_result at the
|
|
183
|
+
// head of the recent window → Anthropic 400. The fix snaps keepStart
|
|
184
|
+
// forward past the complete pair.
|
|
185
|
+
const toolCall = {
|
|
186
|
+
id: 't1',
|
|
187
|
+
type: 'function' as const,
|
|
188
|
+
function: { name: 'read', arguments: '{}' },
|
|
189
|
+
}
|
|
190
|
+
const messages: Message[] = [
|
|
191
|
+
createSystemMessage(`STATIC SYSTEM PROMPT ${FILLER}`, 'cache'),
|
|
192
|
+
createUserMessage(`user 0 ${FILLER}`),
|
|
193
|
+
createAssistantMessage(`assistant 0 ${FILLER}`),
|
|
194
|
+
createUserMessage(`user 1 ${FILLER}`),
|
|
195
|
+
createAssistantMessage(`calling a tool ${FILLER}`, [toolCall]), // idx 4
|
|
196
|
+
createToolMessage(`tool result ${FILLER}`, 't1'), // idx 5 — naive recent[0]
|
|
197
|
+
createAssistantMessage(`assistant 2 ${FILLER}`),
|
|
198
|
+
createUserMessage(`user 3 ${FILLER}`),
|
|
199
|
+
createAssistantMessage(`assistant 3 ${FILLER}`),
|
|
200
|
+
]
|
|
201
|
+
const ctx = makeCtx({ messages, contextWindowTokens: 100 })
|
|
202
|
+
|
|
203
|
+
await runCompactionCheck(ctx)
|
|
204
|
+
|
|
205
|
+
// Compaction fired.
|
|
206
|
+
expect(messages.some((m) => m.content?.includes('[COMPACTED CONTEXT]'))).toBe(true)
|
|
207
|
+
// The surviving transcript has NO dangling tool pair — the tool result
|
|
208
|
+
// whose assistant was summarized away was moved into the older set too,
|
|
209
|
+
// so no orphaned tool_result remains to 400 the next provider turn.
|
|
210
|
+
expect(findDanglingMessages(messages).isValid).toBe(true)
|
|
211
|
+
// And a `tool` message never leads the recent window.
|
|
212
|
+
const firstNonSystem = messages.find((m) => m.role !== 'system')
|
|
213
|
+
expect(firstNonSystem?.role).not.toBe('tool')
|
|
214
|
+
})
|
|
215
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createSystemMessage } from '../../../../types/message/index.js'
|
|
2
|
+
import type { IterationContext } from './context.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sentinel header identifying the single PINNED working-memory system message.
|
|
6
|
+
*
|
|
7
|
+
* The SDK finds + replaces the slot by this header every turn and uses the same
|
|
8
|
+
* identity to re-pin it if compaction ever drops it (the OPAQUE survival guard
|
|
9
|
+
* in `compaction.ts` — no host-format knowledge required). The host's rendered
|
|
10
|
+
* block is stored verbatim AFTER this header; the SDK never parses it.
|
|
11
|
+
*/
|
|
12
|
+
export const WORKING_MEMORY_HEADER =
|
|
13
|
+
'[WORKING MEMORY] Authoritative state for this conversation — you produced these.'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* True when `content` is the pinned working-memory slot (header identity only).
|
|
17
|
+
* Shared with `compaction.ts` so the survival guard re-pins by the SAME rule.
|
|
18
|
+
*/
|
|
19
|
+
export function isWorkingMemoryMessage(content: string | null | undefined): boolean {
|
|
20
|
+
return typeof content === 'string' && content.startsWith(WORKING_MEMORY_HEADER)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the host's working-memory string and rewrite a single PINNED leading
|
|
25
|
+
* system message in place. Insert-or-replace keyed by {@link WORKING_MEMORY_HEADER};
|
|
26
|
+
* an empty/blank string removes the slot.
|
|
27
|
+
*
|
|
28
|
+
* The slot is an EPHEMERAL system message placed as the LAST leading system
|
|
29
|
+
* message (after the cached static + dynamic system messages), so it never
|
|
30
|
+
* busts the prompt-cache prefix yet still rides inside the compaction-preserved
|
|
31
|
+
* leading-system run (the primacy edge Lost-in-the-Middle / Context-Rot need).
|
|
32
|
+
*
|
|
33
|
+
* Failure-isolated (the `web-search` seam rule): a throwing/slow provider
|
|
34
|
+
* degrades to "no refresh this turn" (keeps the prior slot), never breaks the
|
|
35
|
+
* run. Early-returns when no provider is configured ⇒ byte-identical run path.
|
|
36
|
+
*/
|
|
37
|
+
export async function refreshWorkingMemory(ctx: IterationContext): Promise<void> {
|
|
38
|
+
const provider = ctx.workingMemoryProvider
|
|
39
|
+
if (!provider) return
|
|
40
|
+
|
|
41
|
+
let block: string
|
|
42
|
+
try {
|
|
43
|
+
block =
|
|
44
|
+
(await provider({
|
|
45
|
+
runId: ctx.runMgr.id,
|
|
46
|
+
iteration: ctx.runMgr.currentIteration,
|
|
47
|
+
})) ?? ''
|
|
48
|
+
} catch (err) {
|
|
49
|
+
ctx.log.warn('workingMemoryProvider failed; keeping prior slot', {
|
|
50
|
+
runId: ctx.runMgr.id,
|
|
51
|
+
error: err instanceof Error ? err.message : String(err),
|
|
52
|
+
})
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const msgs = ctx.runMgr.messages
|
|
57
|
+
|
|
58
|
+
// Bound the search to the LEADING system run (the compaction-preserved
|
|
59
|
+
// region). A working-memory header appearing later in the transcript (e.g.
|
|
60
|
+
// echoed by a tool result) must not be mistaken for the pinned slot.
|
|
61
|
+
let leadEnd = 0
|
|
62
|
+
while (leadEnd < msgs.length && msgs[leadEnd]?.role === 'system') leadEnd++
|
|
63
|
+
|
|
64
|
+
let idx = -1
|
|
65
|
+
for (let i = 0; i < leadEnd; i++) {
|
|
66
|
+
const m = msgs[i]
|
|
67
|
+
if (m && m.role === 'system' && isWorkingMemoryMessage(m.content)) {
|
|
68
|
+
idx = i
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!block.trim()) {
|
|
74
|
+
// Empty block ⇒ remove the slot (byte-identical-when-empty).
|
|
75
|
+
if (idx >= 0) msgs.splice(idx, 1)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const wm = createSystemMessage(`${WORKING_MEMORY_HEADER}\n\n${block}`, 'ephemeral')
|
|
80
|
+
if (idx >= 0) {
|
|
81
|
+
msgs[idx] = wm
|
|
82
|
+
} else {
|
|
83
|
+
// Insert as the LAST leading system message (after static + dynamic).
|
|
84
|
+
msgs.splice(leadEnd, 0, wm)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { mergeTokenUsage } from '../../../types/common/index.js'
|
|
2
|
+
import type { ToolUseId } from '../../../types/ids/index.js'
|
|
3
|
+
import type {
|
|
4
|
+
ChatCompletionResponse,
|
|
5
|
+
LLMProvider,
|
|
6
|
+
StreamChunk,
|
|
7
|
+
} from '../../../types/provider/index.js'
|
|
8
|
+
import type { RunEvent } from '../../../types/run/index.js'
|
|
9
|
+
import type { MessageStopReason } from '../../../types/run/stop-reason.js'
|
|
10
|
+
import { generateMessageId } from '../../../utils/id.js'
|
|
11
|
+
import type { Logger } from '../../../utils/logger.js'
|
|
12
|
+
import type { EmitEvent } from '../events.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Map a provider's coarse `finishReason` plus the orchestrator's
|
|
16
|
+
* `forceFinalize` flag onto the per-message {@link MessageStopReason}
|
|
17
|
+
* union the v3 `message_completed` event surfaces.
|
|
18
|
+
*/
|
|
19
|
+
function synthesizeMessageStopReason(
|
|
20
|
+
finishReason: 'stop' | 'tool_calls' | 'length' | 'content_filter',
|
|
21
|
+
forceFinalize: boolean,
|
|
22
|
+
): MessageStopReason {
|
|
23
|
+
if (forceFinalize) return 'forced_finalize'
|
|
24
|
+
switch (finishReason) {
|
|
25
|
+
case 'tool_calls':
|
|
26
|
+
return 'tool_use'
|
|
27
|
+
case 'length':
|
|
28
|
+
return 'max_tokens'
|
|
29
|
+
case 'content_filter':
|
|
30
|
+
return 'refusal'
|
|
31
|
+
default:
|
|
32
|
+
return 'end_turn'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface StreamingTurnResult {
|
|
37
|
+
response: ChatCompletionResponse
|
|
38
|
+
messageId: import('../../../types/ids/index.js').MessageId
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Consume a provider's streaming response and emit the v3 RunEvent
|
|
43
|
+
* lifecycle natively (message_started → text_delta* + tool_input_*
|
|
44
|
+
* → message_completed). Returns the aggregated `ChatCompletionResponse`
|
|
45
|
+
* for downstream code that still expects the legacy shape (assistant
|
|
46
|
+
* message construction, working-state extraction, telemetry attribute
|
|
47
|
+
* stamping).
|
|
48
|
+
*
|
|
49
|
+
* Per-delta `emitEvent` calls are followed by a `drainPending()`
|
|
50
|
+
* yield so SSE consumers see live progress instead of a burst at
|
|
51
|
+
* end-of-message. The bus's ephemeral filter (D1) ensures these
|
|
52
|
+
* deltas never hit transcript.jsonl.
|
|
53
|
+
*
|
|
54
|
+
* Edge cases (codex A3, A4, A5):
|
|
55
|
+
* - Stream ends without `finishReason` (anthropic-sdk-typescript#842
|
|
56
|
+
* dropped message_stop): we still emit `message_completed` from a
|
|
57
|
+
* finally-style fall-through path with `stopReason: 'refusal'`.
|
|
58
|
+
* - `tool_input_delta` with no `toolUseId` registered yet: we drop
|
|
59
|
+
* the fragment and log a warning (proxies seen to misorder events).
|
|
60
|
+
* - `chunk.error`: when no tool input is recoverable, we surface as
|
|
61
|
+
* a thrown error after emitting the message_completed terminator so
|
|
62
|
+
* consumer cards still close. If a tool-use block was already open,
|
|
63
|
+
* we instead synthesize a tool call with runtime truncation metadata
|
|
64
|
+
* so the executor can return a model-readable retry hint.
|
|
65
|
+
*/
|
|
66
|
+
export async function* streamProviderTurn(
|
|
67
|
+
provider: LLMProvider,
|
|
68
|
+
params: import('../../../types/provider/index.js').ChatCompletionParams,
|
|
69
|
+
emitEvent: EmitEvent,
|
|
70
|
+
drainPending: () => Generator<RunEvent>,
|
|
71
|
+
runId: import('../../../types/ids/index.js').RunId,
|
|
72
|
+
iteration: number,
|
|
73
|
+
forceFinalize: boolean,
|
|
74
|
+
log: Logger,
|
|
75
|
+
): AsyncGenerator<RunEvent, StreamingTurnResult> {
|
|
76
|
+
const messageId = generateMessageId()
|
|
77
|
+
await emitEvent({ type: 'message_started', runId, iteration, messageId })
|
|
78
|
+
yield* drainPending()
|
|
79
|
+
|
|
80
|
+
let id = ''
|
|
81
|
+
const model = ''
|
|
82
|
+
let textBuf = ''
|
|
83
|
+
let finishReason: ChatCompletionResponse['finishReason'] = 'stop'
|
|
84
|
+
let usage: ChatCompletionResponse['usage'] = {
|
|
85
|
+
promptTokens: 0,
|
|
86
|
+
completionTokens: 0,
|
|
87
|
+
totalTokens: 0,
|
|
88
|
+
cachedTokens: 0,
|
|
89
|
+
cacheWriteTokens: 0,
|
|
90
|
+
}
|
|
91
|
+
const toolBuckets = new Map<
|
|
92
|
+
number,
|
|
93
|
+
{
|
|
94
|
+
id: string
|
|
95
|
+
name: string
|
|
96
|
+
argsBuf: string
|
|
97
|
+
started: boolean
|
|
98
|
+
completed: boolean
|
|
99
|
+
/**
|
|
100
|
+
* Parsed input. `null` while the bucket is still streaming.
|
|
101
|
+
* The synthesized
|
|
102
|
+
* `ChatCompletionResponse.toolCalls[].function.arguments` is
|
|
103
|
+
* derived from this — never from the raw buffer — so the
|
|
104
|
+
* downstream executor (`runtime/query/executor.ts`) never has
|
|
105
|
+
* to re-parse a truncated string. A truncated tool call is
|
|
106
|
+
* surfaced as `arguments: "{}"` plus `metadata.inputTruncated`
|
|
107
|
+
* so tool args remain clean while the executor can still
|
|
108
|
+
* return a specific retry hint.
|
|
109
|
+
*/
|
|
110
|
+
parsed: unknown | null
|
|
111
|
+
inputTruncated: boolean
|
|
112
|
+
}
|
|
113
|
+
>()
|
|
114
|
+
let streamError: string | undefined
|
|
115
|
+
|
|
116
|
+
const stream = provider.chatStream({ ...params, stream: true }) as AsyncIterable<StreamChunk>
|
|
117
|
+
|
|
118
|
+
// Drive the stream manually so each `.next()` can be RACED against the run
|
|
119
|
+
// abort: a Stop tears the in-flight model request down (the provider got
|
|
120
|
+
// `params.signal`), and we ALSO stop pulling within a tick even if a
|
|
121
|
+
// transport buffers or ignores the signal. The abort rejection propagates
|
|
122
|
+
// out of this generator so the run loop settles the turn as cancelled.
|
|
123
|
+
// `{ once: true }` keeps a multi-iteration run from leaking a listener/turn.
|
|
124
|
+
const it = stream[Symbol.asyncIterator]()
|
|
125
|
+
const signal = params.signal
|
|
126
|
+
let onAbort: (() => void) | undefined
|
|
127
|
+
const aborted: Promise<never> | undefined = signal
|
|
128
|
+
? new Promise<never>((_resolve, reject) => {
|
|
129
|
+
if (signal.aborted) {
|
|
130
|
+
reject(signal.reason)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
onAbort = () => reject(signal.reason)
|
|
134
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
135
|
+
})
|
|
136
|
+
: undefined
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
for (;;) {
|
|
140
|
+
const next = it.next()
|
|
141
|
+
// Neutralize the dangling loser so an eventual rejection of the
|
|
142
|
+
// un-awaited `next` is never an unhandled rejection.
|
|
143
|
+
if (aborted) next.catch(() => {})
|
|
144
|
+
const res = await (aborted ? Promise.race([next, aborted]) : next)
|
|
145
|
+
if (res.done) break
|
|
146
|
+
const chunk = res.value
|
|
147
|
+
if (chunk.error) {
|
|
148
|
+
streamError = chunk.error
|
|
149
|
+
break
|
|
150
|
+
}
|
|
151
|
+
if (!id && chunk.id) id = chunk.id
|
|
152
|
+
|
|
153
|
+
if (chunk.delta.content) {
|
|
154
|
+
textBuf += chunk.delta.content
|
|
155
|
+
await emitEvent({
|
|
156
|
+
type: 'text_delta',
|
|
157
|
+
runId,
|
|
158
|
+
iteration,
|
|
159
|
+
messageId,
|
|
160
|
+
text: chunk.delta.content,
|
|
161
|
+
})
|
|
162
|
+
yield* drainPending()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const tc of chunk.delta.toolCalls ?? []) {
|
|
166
|
+
let bucket = toolBuckets.get(tc.index)
|
|
167
|
+
if (!bucket) {
|
|
168
|
+
bucket = {
|
|
169
|
+
id: tc.id ?? '',
|
|
170
|
+
name: tc.function?.name ?? '',
|
|
171
|
+
argsBuf: '',
|
|
172
|
+
started: false,
|
|
173
|
+
completed: false,
|
|
174
|
+
parsed: null,
|
|
175
|
+
inputTruncated: false,
|
|
176
|
+
}
|
|
177
|
+
toolBuckets.set(tc.index, bucket)
|
|
178
|
+
}
|
|
179
|
+
if (tc.id && !bucket.id) bucket.id = tc.id
|
|
180
|
+
if (tc.function?.name && !bucket.name) bucket.name = tc.function.name
|
|
181
|
+
|
|
182
|
+
if (!bucket.started && bucket.id && bucket.name) {
|
|
183
|
+
bucket.started = true
|
|
184
|
+
await emitEvent({
|
|
185
|
+
type: 'tool_input_started',
|
|
186
|
+
runId,
|
|
187
|
+
iteration,
|
|
188
|
+
messageId,
|
|
189
|
+
toolUseId: bucket.id as ToolUseId,
|
|
190
|
+
toolName: bucket.name,
|
|
191
|
+
})
|
|
192
|
+
yield* drainPending()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const fragment = tc.function?.arguments
|
|
196
|
+
if (fragment) {
|
|
197
|
+
if (!bucket.id) {
|
|
198
|
+
log.warn('tool_input_delta arrived before tool id was known; dropping fragment', {
|
|
199
|
+
runId,
|
|
200
|
+
index: tc.index,
|
|
201
|
+
length: fragment.length,
|
|
202
|
+
})
|
|
203
|
+
} else {
|
|
204
|
+
bucket.argsBuf += fragment
|
|
205
|
+
await emitEvent({
|
|
206
|
+
type: 'tool_input_delta',
|
|
207
|
+
runId,
|
|
208
|
+
toolUseId: bucket.id as ToolUseId,
|
|
209
|
+
partialJson: fragment,
|
|
210
|
+
})
|
|
211
|
+
yield* drainPending()
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (chunk.delta.toolCallEnd) {
|
|
217
|
+
const { index, id: endId } = chunk.delta.toolCallEnd
|
|
218
|
+
const bucket = toolBuckets.get(index)
|
|
219
|
+
if (bucket && !bucket.completed) {
|
|
220
|
+
bucket.completed = true
|
|
221
|
+
let parsed: unknown = {}
|
|
222
|
+
try {
|
|
223
|
+
parsed = bucket.argsBuf ? JSON.parse(bucket.argsBuf) : {}
|
|
224
|
+
} catch (err) {
|
|
225
|
+
bucket.inputTruncated = true
|
|
226
|
+
log.warn('tool input JSON parse failed at content_block_stop', {
|
|
227
|
+
runId,
|
|
228
|
+
toolUseId: endId,
|
|
229
|
+
error: err instanceof Error ? err.message : String(err),
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
bucket.parsed = parsed
|
|
233
|
+
await emitEvent({
|
|
234
|
+
type: 'tool_input_completed',
|
|
235
|
+
runId,
|
|
236
|
+
toolUseId: endId as ToolUseId,
|
|
237
|
+
input: parsed,
|
|
238
|
+
...(bucket.inputTruncated ? { inputTruncated: true } : {}),
|
|
239
|
+
})
|
|
240
|
+
yield* drainPending()
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (chunk.finishReason) finishReason = chunk.finishReason
|
|
245
|
+
// Merge (per-field max), not last-write-wins: a late usage frame that
|
|
246
|
+
// omits input/cache tokens must not zero the counts seen earlier in the
|
|
247
|
+
// stream, which would under-report this turn's accumulated usage.
|
|
248
|
+
if (chunk.usage) usage = mergeTokenUsage(usage, chunk.usage)
|
|
249
|
+
}
|
|
250
|
+
} catch (err) {
|
|
251
|
+
// An abort tears the turn down: propagate it so the run loop settles the
|
|
252
|
+
// run as cancelled rather than recording a normal (errored) turn. Any
|
|
253
|
+
// other stream error is captured into the synthesized response as before.
|
|
254
|
+
if (signal?.aborted) throw err
|
|
255
|
+
streamError = err instanceof Error ? err.message : String(err)
|
|
256
|
+
} finally {
|
|
257
|
+
if (onAbort) signal?.removeEventListener('abort', onAbort)
|
|
258
|
+
// Release the underlying connection on every exit (natural end, error,
|
|
259
|
+
// or abort). `for await` did this implicitly on natural completion; the
|
|
260
|
+
// manual drive must do it explicitly. `.return()` on an already-finished
|
|
261
|
+
// provider generator is a no-op.
|
|
262
|
+
await it.return?.().catch(() => {})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Flush any tool buckets the provider failed to close (no toolCallEnd
|
|
266
|
+
// arrived — defensive against providers that don't yet emit it, and
|
|
267
|
+
// the load-bearing path when the provider stream ends with
|
|
268
|
+
// `stop_reason: "max_tokens"` mid-`input_json_delta`. In that case
|
|
269
|
+
// Anthropic's SSE never sends `content_block_stop` for the open
|
|
270
|
+
// tool_use block: the upstream model ran out of completion tokens
|
|
271
|
+
// before it could close the JSON literal, so the buffered
|
|
272
|
+
// `argsBuf` ends with something like `"content":"…some prefix` —
|
|
273
|
+
// not parseable.
|
|
274
|
+
//
|
|
275
|
+
// Two cases coalesce here:
|
|
276
|
+
// 1. The buffer parses cleanly (the provider just forgot to emit
|
|
277
|
+
// `content_block_stop` but the args are intact) — keep parsed.
|
|
278
|
+
// 2. The buffer is truncated mid-literal — `parsed = {}` is the
|
|
279
|
+
// safe fallback so the executor's `JSON.parse(arguments)`
|
|
280
|
+
// succeeds and downstream consumers don't crash. The PRICE
|
|
281
|
+
// we used to pay was the model getting back a generic
|
|
282
|
+
// "<field> is required" Zod error and not realising its
|
|
283
|
+
// previous tool call was truncated server-side, so it would
|
|
284
|
+
// retry with the SAME long input and hit the same cutoff in
|
|
285
|
+
// a loop. Detect the truncation case and mark the tool call
|
|
286
|
+
// with runtime metadata; the executor surfaces a specific
|
|
287
|
+
// "your tool call was cut off by max_tokens — retry with
|
|
288
|
+
// shorter input or split into smaller calls" message that the
|
|
289
|
+
// model can act on.
|
|
290
|
+
for (const bucket of toolBuckets.values()) {
|
|
291
|
+
if (bucket.started && !bucket.completed) {
|
|
292
|
+
bucket.completed = true
|
|
293
|
+
let parsed: unknown = {}
|
|
294
|
+
let truncated = false
|
|
295
|
+
if (bucket.argsBuf) {
|
|
296
|
+
try {
|
|
297
|
+
parsed = JSON.parse(bucket.argsBuf)
|
|
298
|
+
} catch {
|
|
299
|
+
// argsBuf had content but didn't parse — almost
|
|
300
|
+
// certainly the max_tokens-mid-literal cutoff. Mark
|
|
301
|
+
// the bucket so the executor can return a model-
|
|
302
|
+
// readable hint instead of a generic Zod error.
|
|
303
|
+
truncated = true
|
|
304
|
+
parsed = {}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
bucket.parsed = parsed
|
|
308
|
+
bucket.inputTruncated = truncated
|
|
309
|
+
if (truncated) {
|
|
310
|
+
log.warn('tool input truncated by upstream cutoff (no toolCallEnd, argsBuf unparsable)', {
|
|
311
|
+
runId,
|
|
312
|
+
toolUseId: bucket.id,
|
|
313
|
+
toolName: bucket.name,
|
|
314
|
+
bufferLength: bucket.argsBuf.length,
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
await emitEvent({
|
|
318
|
+
type: 'tool_input_completed',
|
|
319
|
+
runId,
|
|
320
|
+
toolUseId: bucket.id as ToolUseId,
|
|
321
|
+
input: parsed,
|
|
322
|
+
...(truncated ? { inputTruncated: true } : {}),
|
|
323
|
+
})
|
|
324
|
+
yield* drainPending()
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// `arguments` MUST be valid JSON for the executor's `JSON.parse`
|
|
329
|
+
// (`runtime/query/executor.ts:executeSingle`) to succeed. We
|
|
330
|
+
// always serialise from the bucket's `parsed` object (filled by
|
|
331
|
+
// either the `toolCallEnd` branch above or the post-stream flush
|
|
332
|
+
// loop) instead of re-emitting `argsBuf`. When the provider
|
|
333
|
+
// stream truncated mid-input, `metadata.inputTruncated` carries that
|
|
334
|
+
// state; the executor parses cleanly and returns a specific
|
|
335
|
+
// model-readable retry hint instead of the generic "Invalid JSON in
|
|
336
|
+
// tool arguments" intercept.
|
|
337
|
+
const toolCalls = [...toolBuckets.entries()]
|
|
338
|
+
.sort(([a], [b]) => a - b)
|
|
339
|
+
.map(([, b]) => ({
|
|
340
|
+
id: b.id,
|
|
341
|
+
type: 'function' as const,
|
|
342
|
+
function: {
|
|
343
|
+
name: b.name,
|
|
344
|
+
arguments: JSON.stringify(b.parsed ?? {}),
|
|
345
|
+
},
|
|
346
|
+
...(b.inputTruncated ? { metadata: { inputTruncated: true } } : {}),
|
|
347
|
+
}))
|
|
348
|
+
|
|
349
|
+
const recoveredToolInputFromStreamError =
|
|
350
|
+
streamError !== undefined && toolCalls.some((tc) => tc.id && tc.function.name)
|
|
351
|
+
const effectiveFinishReason: ChatCompletionResponse['finishReason'] =
|
|
352
|
+
recoveredToolInputFromStreamError ? 'tool_calls' : finishReason
|
|
353
|
+
|
|
354
|
+
if (recoveredToolInputFromStreamError) {
|
|
355
|
+
log.warn('provider stream failed after tool input; surfacing tool call to executor', {
|
|
356
|
+
runId,
|
|
357
|
+
iteration,
|
|
358
|
+
error: streamError,
|
|
359
|
+
toolCallCount: toolCalls.length,
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const stopReason: MessageStopReason = streamError
|
|
364
|
+
? recoveredToolInputFromStreamError
|
|
365
|
+
? 'tool_use'
|
|
366
|
+
: 'refusal'
|
|
367
|
+
: synthesizeMessageStopReason(effectiveFinishReason, forceFinalize)
|
|
368
|
+
|
|
369
|
+
await emitEvent({
|
|
370
|
+
type: 'message_completed',
|
|
371
|
+
runId,
|
|
372
|
+
iteration,
|
|
373
|
+
messageId,
|
|
374
|
+
stopReason,
|
|
375
|
+
usage,
|
|
376
|
+
content: textBuf || undefined,
|
|
377
|
+
})
|
|
378
|
+
yield* drainPending()
|
|
379
|
+
|
|
380
|
+
if (streamError && !recoveredToolInputFromStreamError) {
|
|
381
|
+
throw new Error(`Provider stream error: ${streamError}`)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const response: ChatCompletionResponse = {
|
|
385
|
+
id: id || messageId,
|
|
386
|
+
model: model || params.model,
|
|
387
|
+
message: {
|
|
388
|
+
role: 'assistant',
|
|
389
|
+
content: textBuf.length > 0 ? textBuf : null,
|
|
390
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
391
|
+
},
|
|
392
|
+
finishReason: effectiveFinishReason,
|
|
393
|
+
usage,
|
|
394
|
+
}
|
|
395
|
+
return { response, messageId }
|
|
396
|
+
}
|