@namzu/sdk 1.0.0 → 1.1.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 +167 -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/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/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/collect.d.ts.map +1 -1
- package/dist/provider/collect.js +4 -1
- package/dist/provider/collect.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 +1 -1
- 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/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__/prompt.test.js +15 -11
- package/dist/runtime/query/__tests__/prompt.test.js.map +1 -1
- package/dist/runtime/query/index.d.ts +9 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +23 -2
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +2 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +99 -6
- 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 +2 -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 +12 -0
- 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/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/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/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/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/persona/assembler.ts +18 -6
- package/src/provider/collect.ts +4 -1
- package/src/public-runtime.ts +1 -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/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__/prompt.test.ts +15 -11
- package/src/runtime/query/index.ts +34 -2
- package/src/runtime/query/iteration/index.ts +98 -6
- package/src/runtime/query/iteration/phases/checkpoint.ts +7 -2
- package/src/runtime/query/iteration/phases/compaction.ts +89 -3
- package/src/runtime/query/iteration/phases/context.ts +58 -0
- 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/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/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/version.ts +26 -4
|
@@ -2,7 +2,7 @@ import { createUserMessage } from '../../../../types/message/index.js'
|
|
|
2
2
|
import type { ChatCompletionResponse } from '../../../../types/provider/index.js'
|
|
3
3
|
import type { RunEvent } from '../../../../types/run/index.js'
|
|
4
4
|
import type { VerificationGate } from '../../../../verification/index.js'
|
|
5
|
-
import type
|
|
5
|
+
import { type IterationContext, awaitDecisionOrAbort } from './context.js'
|
|
6
6
|
|
|
7
7
|
interface VerificationAwareContext extends IterationContext {
|
|
8
8
|
readonly verificationGate?: VerificationGate
|
|
@@ -94,7 +94,7 @@ export async function* runToolReview(
|
|
|
94
94
|
})
|
|
95
95
|
yield* ctx.drainPending()
|
|
96
96
|
|
|
97
|
-
const reviewDecision = await ctx
|
|
97
|
+
const reviewDecision = await awaitDecisionOrAbort(ctx, {
|
|
98
98
|
type: 'tool_review',
|
|
99
99
|
runId: ctx.runMgr.id,
|
|
100
100
|
checkpointId: reviewCheckpoint.id,
|
|
@@ -198,7 +198,11 @@ export async function* runToolReview(
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
case 'approve_plan':
|
|
201
|
-
case 'reject_plan':
|
|
201
|
+
case 'reject_plan':
|
|
202
|
+
// 'answer_question' belongs to an ask_user_question park, not a
|
|
203
|
+
// tool review — like the misdirected plan decisions above, warn
|
|
204
|
+
// and proceed with execution rather than stalling the run.
|
|
205
|
+
case 'answer_question': {
|
|
202
206
|
ctx.log.warn('Unexpected plan decision during tool review', {
|
|
203
207
|
action: reviewDecision.action,
|
|
204
208
|
})
|
|
@@ -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
|
+
}
|
|
@@ -148,7 +148,7 @@ describe('SharedRunWorkspace', () => {
|
|
|
148
148
|
runtimeRoot: '/mnt/user-data/outputs/_work',
|
|
149
149
|
})
|
|
150
150
|
|
|
151
|
-
const big =
|
|
151
|
+
const big = `# Task Context\n\n${'A'.repeat(40_000)}`
|
|
152
152
|
const path = await workspace.writeTaskContext(big)
|
|
153
153
|
expect(path).toBe('/mnt/user-data/outputs/_work/01_task_context.md')
|
|
154
154
|
expect(workspace.refs().taskContextPath).toBe(path)
|
|
@@ -295,6 +295,7 @@ function ensureTrailingNewline(value: string): string {
|
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
async function writeJsonAtomic(path: string, value: unknown): Promise<void> {
|
|
298
|
+
await mkdir(dirnameFor(path), { recursive: true })
|
|
298
299
|
const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`
|
|
299
300
|
await writeFile(tmpPath, `${JSON.stringify(value, null, 2)}\n`, 'utf8')
|
|
300
301
|
await rename(tmpPath, path)
|
|
@@ -16,11 +16,11 @@ describe('Agent Skills loader', () => {
|
|
|
16
16
|
const root = await mkdtemp(join(tmpdir(), 'namzu-skills-'))
|
|
17
17
|
const dir = await writeSkill(
|
|
18
18
|
root,
|
|
19
|
-
'
|
|
19
|
+
'delivery-briefing',
|
|
20
20
|
[
|
|
21
21
|
'---',
|
|
22
|
-
'name:
|
|
23
|
-
'description: Draft and edit grounded
|
|
22
|
+
'name: delivery-briefing',
|
|
23
|
+
'description: Draft and edit grounded delivery briefings.',
|
|
24
24
|
'license: MIT',
|
|
25
25
|
'compatibility: Requires filesystem tools',
|
|
26
26
|
'allowed-tools: read write edit',
|
|
@@ -36,8 +36,8 @@ describe('Agent Skills loader', () => {
|
|
|
36
36
|
const result = await loadSkill(dir, 'metadata')
|
|
37
37
|
|
|
38
38
|
expect(result.skill.metadata).toEqual({
|
|
39
|
-
name: '
|
|
40
|
-
description: 'Draft and edit grounded
|
|
39
|
+
name: 'delivery-briefing',
|
|
40
|
+
description: 'Draft and edit grounded delivery briefings.',
|
|
41
41
|
license: 'MIT',
|
|
42
42
|
compatibility: 'Requires filesystem tools',
|
|
43
43
|
allowedTools: 'read write edit',
|
|
@@ -53,11 +53,11 @@ describe('Agent Skills loader', () => {
|
|
|
53
53
|
const root = await mkdtemp(join(tmpdir(), 'namzu-skills-'))
|
|
54
54
|
const dir = await writeSkill(
|
|
55
55
|
root,
|
|
56
|
-
'
|
|
56
|
+
'structured-file-authoring',
|
|
57
57
|
[
|
|
58
58
|
'---',
|
|
59
|
-
'name:
|
|
60
|
-
'description: Create
|
|
59
|
+
'name: structured-file-authoring',
|
|
60
|
+
'description: Create structured files with bounded edit chunks.',
|
|
61
61
|
'---',
|
|
62
62
|
'Use skeleton-first writes.',
|
|
63
63
|
'',
|
|
@@ -117,14 +117,14 @@ describe('coalesce()', () => {
|
|
|
117
117
|
})
|
|
118
118
|
|
|
119
119
|
it('emits new event after window expires', async () => {
|
|
120
|
-
const events: RunEvent
|
|
120
|
+
const events: [RunEvent, RunEvent] = [
|
|
121
121
|
{ type: 'text_delta', runId: RID, iteration: 0, messageId: MID, text: 'a' },
|
|
122
122
|
{ type: 'text_delta', runId: RID, iteration: 0, messageId: MID, text: 'b' },
|
|
123
123
|
]
|
|
124
124
|
const stream: AsyncIterable<RunEvent> = (async function* () {
|
|
125
|
-
yield events[0]
|
|
125
|
+
yield events[0]
|
|
126
126
|
await new Promise((r) => setTimeout(r, 30))
|
|
127
|
-
yield events[1]
|
|
127
|
+
yield events[1]
|
|
128
128
|
})()
|
|
129
129
|
const result = await drain(coalesce(stream, { windowMs: 16 }))
|
|
130
130
|
expect(result).toHaveLength(2)
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
1
4
|
import { describe, expect, it } from 'vitest'
|
|
5
|
+
import type { ToolContext } from '../../../types/tool/index.js'
|
|
2
6
|
import { EditTool } from '../edit.js'
|
|
3
7
|
import { getBuiltinTools } from '../index.js'
|
|
4
8
|
import { WriteFileTool } from '../write-file.js'
|
|
@@ -26,4 +30,63 @@ describe('filesystem tool payload budgeting', () => {
|
|
|
26
30
|
expect(names).toContain('edit')
|
|
27
31
|
expect(names).not.toContain('append')
|
|
28
32
|
})
|
|
33
|
+
|
|
34
|
+
it('assembles long documents with bounded write plus edit insert chunks', async () => {
|
|
35
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-long-doc-'))
|
|
36
|
+
const ctx: ToolContext = {
|
|
37
|
+
runId: 'run_test' as ToolContext['runId'],
|
|
38
|
+
workingDirectory: dir,
|
|
39
|
+
abortSignal: new AbortController().signal,
|
|
40
|
+
env: {},
|
|
41
|
+
log: () => {},
|
|
42
|
+
}
|
|
43
|
+
const opening = '# Long document regression\n\n{{BODY}}\n'
|
|
44
|
+
const chunks = Array.from({ length: 6 }, (_, sectionIndex) => {
|
|
45
|
+
const lines = Array.from({ length: 45 }, (_, lineIndex) => {
|
|
46
|
+
const section = sectionIndex + 1
|
|
47
|
+
const line = lineIndex + 1
|
|
48
|
+
return `Section ${section}.${line}: this bounded paragraph proves long-form output grows through edit insertions rather than one oversized JSON tool argument.`
|
|
49
|
+
})
|
|
50
|
+
const chunk = [`## Section ${sectionIndex + 1}`, ...lines, ''].join('\n')
|
|
51
|
+
expect(chunk.length).toBeLessThan(12_000)
|
|
52
|
+
return chunk
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const writeResult = await WriteFileTool.execute(
|
|
56
|
+
{ path: 'outputs/regression-long-document.md', content: opening },
|
|
57
|
+
ctx,
|
|
58
|
+
)
|
|
59
|
+
expect(writeResult.success).toBe(true)
|
|
60
|
+
|
|
61
|
+
const firstEdit = await EditTool.execute(
|
|
62
|
+
{
|
|
63
|
+
path: 'outputs/regression-long-document.md',
|
|
64
|
+
oldStr: '{{BODY}}',
|
|
65
|
+
newStr: chunks[0],
|
|
66
|
+
replace_all: false,
|
|
67
|
+
},
|
|
68
|
+
ctx,
|
|
69
|
+
)
|
|
70
|
+
expect(firstEdit.success).toBe(true)
|
|
71
|
+
|
|
72
|
+
for (const chunk of chunks.slice(1)) {
|
|
73
|
+
const result = await EditTool.execute(
|
|
74
|
+
{
|
|
75
|
+
path: 'outputs/regression-long-document.md',
|
|
76
|
+
insertLine: 'end',
|
|
77
|
+
newStr: chunk,
|
|
78
|
+
replace_all: false,
|
|
79
|
+
},
|
|
80
|
+
ctx,
|
|
81
|
+
)
|
|
82
|
+
expect(result.success).toBe(true)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const final = readFileSync(join(dir, 'outputs/regression-long-document.md'), 'utf-8')
|
|
86
|
+
expect(final).not.toContain('{{BODY}}')
|
|
87
|
+
expect(final.split('\n').length).toBeGreaterThan(250)
|
|
88
|
+
expect(final).toContain('## Section 1')
|
|
89
|
+
expect(final).toContain('## Section 6')
|
|
90
|
+
expect(final).toContain('Section 6.45')
|
|
91
|
+
})
|
|
29
92
|
})
|
|
@@ -28,4 +28,16 @@ describe('ReadFileTool', () => {
|
|
|
28
28
|
expect(result.success).toBe(true)
|
|
29
29
|
expect(result.output).toBe('2\ttwo\n3\tthree')
|
|
30
30
|
})
|
|
31
|
+
|
|
32
|
+
it('guides binary Office documents through extractor tooling', async () => {
|
|
33
|
+
const dir = mkdtempSync(join(tmpdir(), 'namzu-read-'))
|
|
34
|
+
writeFileSync(join(dir, 'transcript.docx'), Buffer.from('PK\x03\x04binary-docx'))
|
|
35
|
+
|
|
36
|
+
const result = await ReadFileTool.execute({ path: 'transcript.docx' }, makeContext(dir))
|
|
37
|
+
|
|
38
|
+
expect(result.success).toBe(false)
|
|
39
|
+
expect(result.output).toContain('DOCX document package')
|
|
40
|
+
expect(result.output).toContain('python-docx')
|
|
41
|
+
expect(result.data).toMatchObject({ binary: true })
|
|
42
|
+
})
|
|
31
43
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises'
|
|
2
|
-
import { resolve } from 'node:path'
|
|
2
|
+
import { extname, resolve } from 'node:path'
|
|
3
3
|
import { z } from 'zod'
|
|
4
4
|
import { defineTool } from '../defineTool.js'
|
|
5
5
|
|
|
@@ -35,6 +35,18 @@ export const ReadFileTool = defineTool({
|
|
|
35
35
|
// Sandbox-aware: route through sandbox.readFile() when available
|
|
36
36
|
if (context.sandbox) {
|
|
37
37
|
const buffer = await context.sandbox.readFile(input.path)
|
|
38
|
+
const binaryGuidance = describeStructuredBinaryRead(input.path, buffer)
|
|
39
|
+
if (binaryGuidance) {
|
|
40
|
+
return {
|
|
41
|
+
success: false,
|
|
42
|
+
output: binaryGuidance,
|
|
43
|
+
data: {
|
|
44
|
+
path: input.path,
|
|
45
|
+
sandboxed: true,
|
|
46
|
+
binary: true,
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
}
|
|
38
50
|
const content = buffer.toString('utf-8')
|
|
39
51
|
const lines = content.split('\n')
|
|
40
52
|
|
|
@@ -58,7 +70,19 @@ export const ReadFileTool = defineTool({
|
|
|
58
70
|
}
|
|
59
71
|
|
|
60
72
|
const filePath = resolve(context.workingDirectory, input.path)
|
|
61
|
-
const
|
|
73
|
+
const buffer = await readFile(filePath)
|
|
74
|
+
const binaryGuidance = describeStructuredBinaryRead(filePath, buffer)
|
|
75
|
+
if (binaryGuidance) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
output: binaryGuidance,
|
|
79
|
+
data: {
|
|
80
|
+
path: filePath,
|
|
81
|
+
binary: true,
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const content = buffer.toString('utf-8')
|
|
62
86
|
const lines = content.split('\n')
|
|
63
87
|
|
|
64
88
|
const { start, end } = resolveReadWindow(input, lines.length)
|
|
@@ -80,6 +104,30 @@ export const ReadFileTool = defineTool({
|
|
|
80
104
|
},
|
|
81
105
|
})
|
|
82
106
|
|
|
107
|
+
function describeStructuredBinaryRead(path: string, buffer: Buffer): string | null {
|
|
108
|
+
const ext = extname(path).toLowerCase()
|
|
109
|
+
if (ext === '.docx') return buildStructuredBinaryGuidance(path, 'DOCX', 'python-docx')
|
|
110
|
+
if (ext === '.pptx') return buildStructuredBinaryGuidance(path, 'PPTX', 'python-pptx')
|
|
111
|
+
if (ext === '.xlsx') return buildStructuredBinaryGuidance(path, 'XLSX', 'openpyxl')
|
|
112
|
+
if (ext === '.pdf' || startsWithPdfHeader(buffer)) {
|
|
113
|
+
return buildStructuredBinaryGuidance(path, 'PDF', 'pdftotext or PyMuPDF')
|
|
114
|
+
}
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function startsWithPdfHeader(buffer: Buffer): boolean {
|
|
119
|
+
return buffer.length >= 4 && buffer.subarray(0, 4).toString('utf8') === '%PDF'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function buildStructuredBinaryGuidance(path: string, format: string, extractor: string): string {
|
|
123
|
+
return [
|
|
124
|
+
`The file "${path}" is a ${format} document package, not UTF-8 text.`,
|
|
125
|
+
'Do not use the read/cat tools as evidence for this raw file.',
|
|
126
|
+
`Extract its text with shell/Python tooling already available in the sandbox (${extractor}), write the extracted text or summary under scratch, then read that text file.`,
|
|
127
|
+
'If extraction fails, report the exact filename and extraction error instead of claiming the attachment is unavailable.',
|
|
128
|
+
].join('\n')
|
|
129
|
+
}
|
|
130
|
+
|
|
83
131
|
function resolveReadWindow(
|
|
84
132
|
input: z.infer<typeof inputSchema>,
|
|
85
133
|
totalLines: number,
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
+
import { toolDiscoveryHint } from '../../registry/tool/execute.js'
|
|
2
3
|
import { defineTool } from '../defineTool.js'
|
|
3
4
|
|
|
4
5
|
const inputSchema = z.object({
|
|
5
6
|
query: z.string().describe('Tool name or capability keyword to search for'),
|
|
6
7
|
})
|
|
7
8
|
|
|
9
|
+
// Activate only the best-ranked matches. Top-5 is the literature's sweet
|
|
10
|
+
// spot (retrieval at k=5 matches oracle toolsets) and bounds how much
|
|
11
|
+
// schema weight a single search can add to every subsequent iteration —
|
|
12
|
+
// each activation also busts the prompt-cache prefix once.
|
|
13
|
+
const ACTIVATION_TOP_K = 5
|
|
14
|
+
|
|
15
|
+
// Near-misses are reported name+hint WITHOUT activating, so a retrieval
|
|
16
|
+
// miss becomes one cheap re-query instead of a dead end (the fixed-top-k
|
|
17
|
+
// hard-tail failure mode: the right tool ranked 6th-10th).
|
|
18
|
+
const NEAR_MISS_LIMIT = 5
|
|
19
|
+
|
|
8
20
|
export const SearchToolsTool = defineTool({
|
|
9
21
|
name: 'search_tools',
|
|
10
22
|
description:
|
|
@@ -28,27 +40,46 @@ export const SearchToolsTool = defineTool({
|
|
|
28
40
|
|
|
29
41
|
const allowed =
|
|
30
42
|
context.allowedTools && context.allowedTools.length > 0 ? new Set(context.allowedTools) : null
|
|
31
|
-
|
|
43
|
+
// `searchDeferred` returns a ranked list (score-descending), so slicing
|
|
44
|
+
// the head is a true top-k activation, not an arbitrary subset.
|
|
45
|
+
const ranked = context.toolRegistry
|
|
32
46
|
.searchDeferred(input.query)
|
|
33
47
|
.filter((tool) => !allowed || allowed.has(tool.name))
|
|
34
48
|
|
|
35
|
-
if (
|
|
49
|
+
if (ranked.length === 0) {
|
|
36
50
|
return {
|
|
37
51
|
success: true,
|
|
38
52
|
output: `No deferred tools matching "${input.query}". All matching tools are already active.`,
|
|
39
53
|
}
|
|
40
54
|
}
|
|
41
55
|
|
|
42
|
-
|
|
56
|
+
const activated = ranked.slice(0, ACTIVATION_TOP_K)
|
|
57
|
+
const nearMisses = ranked.slice(ACTIVATION_TOP_K, ACTIVATION_TOP_K + NEAR_MISS_LIMIT)
|
|
58
|
+
|
|
59
|
+
context.toolRegistry.activate(activated.map((t) => t.name))
|
|
43
60
|
|
|
44
|
-
const descriptions =
|
|
61
|
+
const descriptions = activated.map((t) => `- ${t.name}: ${t.description}`).join('\n')
|
|
62
|
+
const sections = [`Activated ${activated.length} tool(s):\n${descriptions}`]
|
|
63
|
+
|
|
64
|
+
if (nearMisses.length > 0) {
|
|
65
|
+
const hints = nearMisses
|
|
66
|
+
.map((t) => {
|
|
67
|
+
const hint = toolDiscoveryHint(t.description)
|
|
68
|
+
return hint.length > 0 ? `- ${t.name}: ${hint}` : `- ${t.name}`
|
|
69
|
+
})
|
|
70
|
+
.join('\n')
|
|
71
|
+
sections.push(
|
|
72
|
+
`Also matched but NOT loaded (search again with a more specific query, e.g. the tool name, to load one of these):\n${hints}`,
|
|
73
|
+
)
|
|
74
|
+
}
|
|
45
75
|
|
|
46
76
|
return {
|
|
47
77
|
success: true,
|
|
48
|
-
output:
|
|
78
|
+
output: sections.join('\n\n'),
|
|
49
79
|
data: {
|
|
50
|
-
activated:
|
|
51
|
-
count:
|
|
80
|
+
activated: activated.map((t) => t.name),
|
|
81
|
+
count: activated.length,
|
|
82
|
+
nearMisses: nearMisses.map((t) => t.name),
|
|
52
83
|
},
|
|
53
84
|
}
|
|
54
85
|
},
|