@namzu/sdk 53.2.2 → 55.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +1 -0
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/compaction/context-breakdown.d.ts +85 -0
- package/dist/compaction/context-breakdown.d.ts.map +1 -0
- package/dist/compaction/context-breakdown.js +149 -0
- package/dist/compaction/context-breakdown.js.map +1 -0
- package/dist/compaction/extractor.d.ts.map +1 -1
- package/dist/compaction/extractor.js +5 -1
- package/dist/compaction/extractor.js.map +1 -1
- package/dist/compaction/managers/structured.d.ts.map +1 -1
- package/dist/compaction/managers/structured.js +5 -0
- package/dist/compaction/managers/structured.js.map +1 -1
- package/dist/compaction/manual.d.ts.map +1 -1
- package/dist/compaction/manual.js +6 -0
- package/dist/compaction/manual.js.map +1 -1
- package/dist/compaction/serializer.d.ts +6 -0
- package/dist/compaction/serializer.d.ts.map +1 -1
- package/dist/compaction/serializer.js +21 -3
- package/dist/compaction/serializer.js.map +1 -1
- package/dist/compaction/token-estimate.d.ts +2 -0
- package/dist/compaction/token-estimate.d.ts.map +1 -1
- package/dist/compaction/token-estimate.js +4 -0
- package/dist/compaction/token-estimate.js.map +1 -1
- package/dist/compaction/verifier.d.ts.map +1 -1
- package/dist/compaction/verifier.js +27 -6
- package/dist/compaction/verifier.js.map +1 -1
- package/dist/constants/compaction/index.d.ts +1 -0
- package/dist/constants/compaction/index.d.ts.map +1 -1
- package/dist/constants/compaction/index.js +1 -0
- package/dist/constants/compaction/index.js.map +1 -1
- package/dist/manager/resident/proposal.d.ts +2 -2
- package/dist/manager/resident/store.d.ts +4 -4
- package/dist/prompt/coding-agent-doctrine.d.ts +3 -3
- package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
- package/dist/prompt/coding-agent-doctrine.js +18 -20
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/public-runtime.d.ts +8 -3
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +7 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/runtime/cache-clock.d.ts +39 -0
- package/dist/runtime/cache-clock.d.ts.map +1 -0
- package/dist/runtime/cache-clock.js +48 -0
- package/dist/runtime/cache-clock.js.map +1 -0
- package/dist/runtime/query/checkpoint.d.ts +12 -2
- package/dist/runtime/query/checkpoint.d.ts.map +1 -1
- package/dist/runtime/query/checkpoint.js +42 -8
- package/dist/runtime/query/checkpoint.js.map +1 -1
- package/dist/runtime/query/compact-session.d.ts +37 -2
- package/dist/runtime/query/compact-session.d.ts.map +1 -1
- package/dist/runtime/query/compact-session.js +77 -3
- package/dist/runtime/query/compact-session.js.map +1 -1
- package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
- package/dist/runtime/query/executor/tool-call-admission.js +3 -0
- package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +39 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +115 -7
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +18 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +20 -1
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +25 -8
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +24 -2
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js +22 -22
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/per-call-review.d.ts +78 -0
- package/dist/runtime/query/iteration/phases/per-call-review.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/per-call-review.js +494 -0
- package/dist/runtime/query/iteration/phases/per-call-review.js.map +1 -0
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +129 -62
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/iteration/step-context.d.ts +14 -0
- package/dist/runtime/query/iteration/step-context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/step-context.js +29 -0
- package/dist/runtime/query/iteration/step-context.js.map +1 -1
- package/dist/runtime/query/iteration/step-shaping.d.ts.map +1 -1
- package/dist/runtime/query/iteration/step-shaping.js +6 -2
- package/dist/runtime/query/iteration/step-shaping.js.map +1 -1
- package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
- package/dist/runtime/query/prepare-turn.js +16 -3
- package/dist/runtime/query/prepare-turn.js.map +1 -1
- package/dist/runtime/query/project-instructions.d.ts +24 -5
- package/dist/runtime/query/project-instructions.d.ts.map +1 -1
- package/dist/runtime/query/project-instructions.js +107 -35
- package/dist/runtime/query/project-instructions.js.map +1 -1
- package/dist/runtime/query/prompt.d.ts +6 -0
- package/dist/runtime/query/prompt.d.ts.map +1 -1
- package/dist/runtime/query/prompt.js +10 -49
- package/dist/runtime/query/prompt.js.map +1 -1
- package/dist/runtime/query/resume-pending.d.ts +8 -0
- package/dist/runtime/query/resume-pending.d.ts.map +1 -1
- package/dist/runtime/query/resume-pending.js +101 -0
- package/dist/runtime/query/resume-pending.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts +18 -0
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +10 -0
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/runtime/query/steering.d.ts +55 -56
- package/dist/runtime/query/steering.d.ts.map +1 -1
- package/dist/runtime/query/steering.js +78 -72
- package/dist/runtime/query/steering.js.map +1 -1
- package/dist/store/budget/disk.js +4 -1
- package/dist/store/budget/disk.js.map +1 -1
- package/dist/tools/admit-facts.d.ts.map +1 -1
- package/dist/tools/admit-facts.js +6 -0
- package/dist/tools/admit-facts.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts +8 -0
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +36 -27
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/browser.d.ts.map +1 -1
- package/dist/tools/builtins/browser.js +38 -12
- package/dist/tools/builtins/browser.js.map +1 -1
- package/dist/tools/builtins/edit-apply.d.ts +7 -4
- package/dist/tools/builtins/edit-apply.d.ts.map +1 -1
- package/dist/tools/builtins/edit-apply.js +3 -0
- package/dist/tools/builtins/edit-apply.js.map +1 -1
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +31 -11
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/exit-semantics.d.ts +31 -0
- package/dist/tools/builtins/exit-semantics.d.ts.map +1 -0
- package/dist/tools/builtins/exit-semantics.js +198 -0
- package/dist/tools/builtins/exit-semantics.js.map +1 -0
- package/dist/tools/builtins/job.js +1 -1
- package/dist/tools/builtins/job.js.map +1 -1
- package/dist/tools/builtins/skill.js +1 -1
- package/dist/tools/builtins/skill.js.map +1 -1
- package/dist/tools/builtins/structuredOutput.js +1 -1
- package/dist/tools/builtins/structuredOutput.js.map +1 -1
- package/dist/tools/builtins/verify-outputs.js +1 -1
- package/dist/tools/builtins/verify-outputs.js.map +1 -1
- package/dist/tools/builtins/write-file.js +11 -6
- package/dist/tools/builtins/write-file.js.map +1 -1
- package/dist/tools/coordinator/ask-user-question.d.ts +6 -0
- package/dist/tools/coordinator/ask-user-question.d.ts.map +1 -1
- package/dist/tools/coordinator/ask-user-question.js +6 -1
- package/dist/tools/coordinator/ask-user-question.js.map +1 -1
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +10 -3
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/memory/delete.js +1 -1
- package/dist/tools/memory/delete.js.map +1 -1
- package/dist/tools/memory/read.js +1 -1
- package/dist/tools/memory/read.js.map +1 -1
- package/dist/tools/memory/save.d.ts.map +1 -1
- package/dist/tools/memory/save.js +4 -3
- package/dist/tools/memory/save.js.map +1 -1
- package/dist/tools/memory/search.js +1 -1
- package/dist/tools/memory/search.js.map +1 -1
- package/dist/tools/memory/update.js +1 -1
- package/dist/tools/memory/update.js.map +1 -1
- package/dist/tools/schedules/loop-tool.d.ts.map +1 -1
- package/dist/tools/schedules/loop-tool.js +4 -2
- package/dist/tools/schedules/loop-tool.js.map +1 -1
- package/dist/tools/schedules/schedule-tool.js +2 -2
- package/dist/tools/schedules/schedule-tool.js.map +1 -1
- package/dist/tools/task/create.js +1 -1
- package/dist/tools/task/create.js.map +1 -1
- package/dist/tools/untrusted-envelope.js +1 -1
- package/dist/tools/untrusted-envelope.js.map +1 -1
- package/dist/toolsets/manager.d.ts.map +1 -1
- package/dist/toolsets/manager.js +6 -1
- package/dist/toolsets/manager.js.map +1 -1
- package/dist/turn/evidence-query.d.ts.map +1 -1
- package/dist/turn/evidence-query.js +16 -1
- package/dist/turn/evidence-query.js.map +1 -1
- package/dist/turn/evidence-recall.js +1 -1
- package/dist/turn/evidence-recall.js.map +1 -1
- package/dist/types/agent/base.d.ts +7 -0
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/common/index.d.ts +19 -0
- package/dist/types/common/index.d.ts.map +1 -1
- package/dist/types/common/index.js +9 -0
- package/dist/types/common/index.js.map +1 -1
- package/dist/types/hitl/index.d.ts +66 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/message/index.d.ts +8 -0
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +12 -2
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/message/notice-frame.d.ts +41 -0
- package/dist/types/message/notice-frame.d.ts.map +1 -0
- package/dist/types/message/notice-frame.js +65 -0
- package/dist/types/message/notice-frame.js.map +1 -0
- package/dist/types/provider/chat.d.ts +8 -0
- package/dist/types/provider/chat.d.ts.map +1 -1
- package/dist/types/provider/config.d.ts +6 -0
- package/dist/types/provider/config.d.ts.map +1 -1
- package/dist/types/session/checkpoint.d.ts +52 -0
- package/dist/types/session/checkpoint.d.ts.map +1 -1
- package/dist/types/session/checkpoint.js +5 -0
- package/dist/types/session/checkpoint.js.map +1 -1
- package/dist/types/session/events.d.ts +9 -0
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js.map +1 -1
- package/dist/types/session/records.d.ts +181 -6
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +1 -1
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/tool/presentation.d.ts +17 -1
- package/dist/types/tool/presentation.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/agents/QueryAgent.ts +1 -0
- package/src/compaction/context-breakdown.ts +228 -0
- package/src/compaction/extractor.ts +8 -1
- package/src/compaction/managers/structured.ts +3 -0
- package/src/compaction/manual.ts +5 -0
- package/src/compaction/serializer.ts +28 -3
- package/src/compaction/token-estimate.ts +5 -0
- package/src/compaction/verifier.ts +32 -6
- package/src/constants/compaction/index.ts +1 -0
- package/src/prompt/coding-agent-doctrine.ts +18 -20
- package/src/public-runtime.ts +24 -2
- package/src/runtime/cache-clock.ts +67 -0
- package/src/runtime/query/checkpoint.ts +49 -8
- package/src/runtime/query/compact-session.ts +101 -3
- package/src/runtime/query/executor/tool-call-admission.ts +3 -0
- package/src/runtime/query/executor.ts +165 -15
- package/src/runtime/query/index.ts +45 -1
- package/src/runtime/query/iteration/index.ts +27 -6
- package/src/runtime/query/iteration/phases/context.ts +37 -11
- package/src/runtime/query/iteration/phases/per-call-review.ts +598 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +168 -83
- package/src/runtime/query/iteration/step-context.ts +29 -0
- package/src/runtime/query/iteration/step-shaping.ts +6 -2
- package/src/runtime/query/prepare-turn.ts +18 -1
- package/src/runtime/query/project-instructions.ts +119 -38
- package/src/runtime/query/prompt.ts +11 -54
- package/src/runtime/query/resume-pending.ts +131 -0
- package/src/runtime/query/review-policy.ts +28 -0
- package/src/runtime/query/steering.ts +100 -96
- package/src/store/budget/disk.ts +4 -1
- package/src/tools/admit-facts.ts +6 -0
- package/src/tools/builtins/bash.ts +38 -28
- package/src/tools/builtins/browser.ts +38 -12
- package/src/tools/builtins/edit-apply.ts +12 -2
- package/src/tools/builtins/edit.ts +38 -12
- package/src/tools/builtins/exit-semantics.ts +198 -0
- package/src/tools/builtins/job.ts +1 -1
- package/src/tools/builtins/skill.ts +1 -1
- package/src/tools/builtins/structuredOutput.ts +1 -1
- package/src/tools/builtins/verify-outputs.ts +1 -1
- package/src/tools/builtins/write-file.ts +11 -6
- package/src/tools/coordinator/ask-user-question.ts +12 -1
- package/src/tools/coordinator/index.ts +10 -3
- package/src/tools/memory/delete.ts +1 -1
- package/src/tools/memory/read.ts +1 -1
- package/src/tools/memory/save.ts +4 -3
- package/src/tools/memory/search.ts +1 -1
- package/src/tools/memory/update.ts +1 -1
- package/src/tools/schedules/loop-tool.ts +4 -2
- package/src/tools/schedules/schedule-tool.ts +2 -2
- package/src/tools/task/create.ts +1 -1
- package/src/tools/untrusted-envelope.ts +1 -1
- package/src/toolsets/manager.ts +6 -1
- package/src/turn/evidence-query.ts +19 -1
- package/src/turn/evidence-recall.ts +1 -1
- package/src/types/agent/base.ts +8 -0
- package/src/types/common/index.ts +27 -0
- package/src/types/hitl/index.ts +67 -0
- package/src/types/message/index.ts +20 -2
- package/src/types/message/notice-frame.ts +88 -0
- package/src/types/provider/chat.ts +8 -0
- package/src/types/provider/config.ts +6 -0
- package/src/types/session/checkpoint.ts +5 -0
- package/src/types/session/events.ts +6 -0
- package/src/types/session/records.ts +1 -1
- package/src/types/tool/presentation.ts +16 -0
|
@@ -150,6 +150,8 @@ function buildShellContext(config: PromptBuilderConfig): string | undefined {
|
|
|
150
150
|
return lines.length > 0 ? lines.join('\n') : undefined
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
const SEGMENT_SEPARATOR = '\n\n---\n\n'
|
|
154
|
+
|
|
153
155
|
export class PromptBuilder {
|
|
154
156
|
private config: PromptBuilderConfig
|
|
155
157
|
|
|
@@ -232,67 +234,22 @@ export class PromptBuilder {
|
|
|
232
234
|
)
|
|
233
235
|
}
|
|
234
236
|
|
|
237
|
+
/**
|
|
238
|
+
* The whole prompt as one string: exactly the two segments a request
|
|
239
|
+
* sends, static then dynamic, joined by the separator. It is derived from
|
|
240
|
+
* `buildSegmented` rather than assembled a second time, so the order the
|
|
241
|
+
* model reads and the text the prompt-cache hash covers cannot drift apart.
|
|
242
|
+
*/
|
|
235
243
|
build(contextLevel: AgentContextLevel = 'full', workingDirectory?: string): string {
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
if (contextLevel === 'full' && this.config.basePrompt) {
|
|
239
|
-
parts.push(this.config.basePrompt)
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (this.config.systemPrompt) {
|
|
243
|
-
parts.push(this.config.systemPrompt)
|
|
244
|
-
} else if (this.config.persona) {
|
|
245
|
-
parts.push(assembleSystemPrompt(this.config.persona, this.config.skills))
|
|
246
|
-
} else {
|
|
247
|
-
const skillSection = this.renderSkillsInPlace()
|
|
248
|
-
if (skillSection) parts.push(skillSection)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (this.config.systemPrompt) {
|
|
252
|
-
const skillSection = this.renderSkillsInPlace()
|
|
253
|
-
if (skillSection) parts.push(skillSection)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (contextLevel !== 'minimal') {
|
|
257
|
-
const toolSection = this.config.tools.toPromptSection(this.config.allowedTools)
|
|
258
|
-
if (toolSection) {
|
|
259
|
-
parts.push(toolSection)
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const tierGuidance = this.config.tools.toTierGuidance()
|
|
263
|
-
if (tierGuidance) {
|
|
264
|
-
parts.push(tierGuidance)
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (contextLevel !== 'minimal' && workingDirectory) {
|
|
269
|
-
const shouldIncludeEnv =
|
|
270
|
-
hasFilesystemTools(this.config.tools, this.config.allowedTools) ||
|
|
271
|
-
Boolean(this.config.runtimeContext)
|
|
272
|
-
if (shouldIncludeEnv) {
|
|
273
|
-
parts.push(buildEnvContext(workingDirectory, this.config))
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Static then dynamic, in that order and nothing between them,
|
|
278
|
-
// because that is exactly how `buildSegmented` is rejoined upstream:
|
|
279
|
-
// `${static}\n\n---\n\n${dynamic}`. The two methods produce the same
|
|
280
|
-
// prompt for the same input, and a turn that hits the prompt cache
|
|
281
|
-
// must not be asking a different question from one that misses it.
|
|
282
|
-
const shellContext = buildShellContext(this.config)
|
|
283
|
-
if (shellContext) parts.push(shellContext)
|
|
284
|
-
|
|
285
|
-
parts.push(...this.renderContributions('static', workingDirectory))
|
|
286
|
-
parts.push(...this.renderContributions('dynamic', workingDirectory))
|
|
287
|
-
|
|
288
|
-
return parts.join('\n\n---\n\n')
|
|
244
|
+
const { static: staticText, dynamic } = this.buildSegmented(contextLevel, workingDirectory)
|
|
245
|
+
return [staticText, dynamic].filter((part) => part.length > 0).join(SEGMENT_SEPARATOR)
|
|
289
246
|
}
|
|
290
247
|
|
|
291
248
|
buildSegmented(
|
|
292
249
|
contextLevel: AgentContextLevel = 'full',
|
|
293
250
|
workingDirectory?: string,
|
|
294
251
|
): PromptSegments {
|
|
295
|
-
const separator =
|
|
252
|
+
const separator = SEGMENT_SEPARATOR
|
|
296
253
|
const staticParts: string[] = []
|
|
297
254
|
const dynamicParts: string[] = []
|
|
298
255
|
|
|
@@ -75,6 +75,12 @@ export interface PendingResumePlan {
|
|
|
75
75
|
readonly modifiedCallIds?: ReadonlySet<string>
|
|
76
76
|
/** Calls whose sandbox escape the durable decision confirmed; see `HITLResumeDecision`. */
|
|
77
77
|
readonly confirmedEscalations?: readonly string[]
|
|
78
|
+
/**
|
|
79
|
+
* The park asked about some calls of the batch (the per-call review), so the
|
|
80
|
+
* decision speaks for those and no others. Every other call that has no
|
|
81
|
+
* recovered result is not run; see {@link applyPendingResume}.
|
|
82
|
+
*/
|
|
83
|
+
readonly partial?: true
|
|
78
84
|
/**
|
|
79
85
|
* Answers to deliver to tools that parked on a question, keyed by the
|
|
80
86
|
* asking call's id. Present only on a question resume.
|
|
@@ -136,6 +142,9 @@ export function planPendingResume(
|
|
|
136
142
|
// describe this state and applying it would be consent-by-coincidence.
|
|
137
143
|
const recordedIds = new Set(pending.request.toolCalls.map((tc) => tc.id))
|
|
138
144
|
const actualIds = assistant.toolCalls.map((tc) => tc.id)
|
|
145
|
+
if (pending.request.perCall === true) {
|
|
146
|
+
return planPartialResume(checkpoint, pending.request.toolCalls, assistant, decision, log)
|
|
147
|
+
}
|
|
139
148
|
const mismatched = actualIds.filter((id) => !recordedIds.has(id))
|
|
140
149
|
if (mismatched.length > 0 || recordedIds.size !== actualIds.length) {
|
|
141
150
|
log.error('Tool calls in the checkpoint do not match the ones the decision was made about', {
|
|
@@ -166,6 +175,118 @@ export function planPendingResume(
|
|
|
166
175
|
}
|
|
167
176
|
}
|
|
168
177
|
|
|
178
|
+
/** What a call is told when it had not run, and nobody had answered it, when the session stopped. */
|
|
179
|
+
export const NOT_RUN_BEFORE_RESTART =
|
|
180
|
+
'This call had not run when the session stopped, and an earlier approval does not carry over. Ask again if it is still needed.'
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Resume a park that asked about some calls of a batch, not all of them.
|
|
184
|
+
*
|
|
185
|
+
* The decision speaks for the calls it was shown, and only those: the id check
|
|
186
|
+
* is that every one of them is still an unanswered call of the batch. Calls
|
|
187
|
+
* that already ran are answered from the record when the plan is applied, and
|
|
188
|
+
* a call that had not run and was not asked about is not run: a yes given to
|
|
189
|
+
* one call is not a yes to another.
|
|
190
|
+
*/
|
|
191
|
+
function planPartialResume(
|
|
192
|
+
checkpoint: RestoredCheckpoint,
|
|
193
|
+
shown: readonly ToolCallSummary[],
|
|
194
|
+
assistant: AssistantMessage,
|
|
195
|
+
decision: HITLResumeDecision,
|
|
196
|
+
log: Logger,
|
|
197
|
+
): PendingResumePlan | null {
|
|
198
|
+
const calls = assistant.toolCalls ?? []
|
|
199
|
+
const byId = new Map(calls.map((call) => [call.id, call]))
|
|
200
|
+
const shownCalls = shown.map((summary) => byId.get(summary.id))
|
|
201
|
+
if (shown.length === 0 || shownCalls.some((call) => call === undefined)) {
|
|
202
|
+
log.error('The call a decision was made about is not among the unanswered tool calls', {
|
|
203
|
+
'namzu.checkpoint.id': checkpoint.id,
|
|
204
|
+
'namzu.runtime.recorded': shown.map((summary) => summary.id),
|
|
205
|
+
'namzu.runtime.actual': calls.map((call) => call.id),
|
|
206
|
+
})
|
|
207
|
+
return null
|
|
208
|
+
}
|
|
209
|
+
const asked = shownCalls as ToolCall[]
|
|
210
|
+
const denials = derivePartialDenials(asked, decision)
|
|
211
|
+
if (!denials) return null
|
|
212
|
+
const declines = derivePartialDeclines(asked, decision, shown)
|
|
213
|
+
return {
|
|
214
|
+
source: 'decision',
|
|
215
|
+
checkpointId: checkpoint.id,
|
|
216
|
+
assistant,
|
|
217
|
+
response: synthesizeResponse(assistant),
|
|
218
|
+
denials,
|
|
219
|
+
...(declines ? { declines } : {}),
|
|
220
|
+
reviewedCalls: shown,
|
|
221
|
+
modifiedCallIds: new Set(),
|
|
222
|
+
partial: true,
|
|
223
|
+
...((decision.action === 'approve_tools' || decision.action === 'modify_tools') &&
|
|
224
|
+
decision.confirmedEscalations
|
|
225
|
+
? { confirmedEscalations: decision.confirmedEscalations }
|
|
226
|
+
: {}),
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The same reading of a decision the live per-call review makes: nothing the
|
|
232
|
+
* decision does not name is approved, and an edit is not offered.
|
|
233
|
+
*/
|
|
234
|
+
function derivePartialDenials(
|
|
235
|
+
asked: readonly ToolCall[],
|
|
236
|
+
decision: HITLResumeDecision,
|
|
237
|
+
): ToolCallDenials | null {
|
|
238
|
+
switch (decision.action) {
|
|
239
|
+
case 'approve_tools':
|
|
240
|
+
case 'continue':
|
|
241
|
+
return new Map()
|
|
242
|
+
case 'reject_tools': {
|
|
243
|
+
const reason = decision.feedback || DECLINED_TOOL_CALL_FEEDBACK
|
|
244
|
+
return new Map(asked.map((call) => [call.id, reason]))
|
|
245
|
+
}
|
|
246
|
+
case 'modify_tools': {
|
|
247
|
+
const denials = new Map<string, string>()
|
|
248
|
+
for (const call of asked) {
|
|
249
|
+
const mods = decision.modifications.filter((mod) => mod.toolCallId === call.id)
|
|
250
|
+
if (mods.some((mod) => mod.action === 'deny'))
|
|
251
|
+
denials.set(call.id, DECLINED_TOOL_CALL_FEEDBACK)
|
|
252
|
+
else if (!mods.some((mod) => mod.action === 'approve')) {
|
|
253
|
+
denials.set(
|
|
254
|
+
call.id,
|
|
255
|
+
'No answer was given for this call, so it was not run. Ask again if it is still needed.',
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return denials
|
|
260
|
+
}
|
|
261
|
+
default:
|
|
262
|
+
return null
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function derivePartialDeclines(
|
|
267
|
+
asked: readonly ToolCall[],
|
|
268
|
+
decision: HITLResumeDecision,
|
|
269
|
+
shown: readonly ToolCallSummary[],
|
|
270
|
+
): ToolCallDeclines | undefined {
|
|
271
|
+
const gateDenied = new Set(
|
|
272
|
+
shown.filter((call) => call.authorization?.decision === 'deny').map((call) => call.id),
|
|
273
|
+
)
|
|
274
|
+
if (decision.action === 'reject_tools' && decision.declined) {
|
|
275
|
+
const declined = decision.declined
|
|
276
|
+
const calls = asked.filter((call) => !gateDenied.has(call.id))
|
|
277
|
+
return calls.length > 0 ? new Map(calls.map((call) => [call.id, declined])) : undefined
|
|
278
|
+
}
|
|
279
|
+
if (decision.action === 'modify_tools') {
|
|
280
|
+
const denied = asked.filter(
|
|
281
|
+
(call) =>
|
|
282
|
+
!gateDenied.has(call.id) &&
|
|
283
|
+
decision.modifications.some((mod) => mod.toolCallId === call.id && mod.action === 'deny'),
|
|
284
|
+
)
|
|
285
|
+
if (denied.length > 0) return new Map(denied.map((call) => [call.id, {}]))
|
|
286
|
+
}
|
|
287
|
+
return undefined
|
|
288
|
+
}
|
|
289
|
+
|
|
169
290
|
/**
|
|
170
291
|
* The stable marker `supersededByRecovery` puts at the head of its reason.
|
|
171
292
|
*
|
|
@@ -432,6 +553,16 @@ export async function applyPendingResume(
|
|
|
432
553
|
const denials = new Map(plan.denials)
|
|
433
554
|
const reviewedById = new Map(plan.reviewedCalls?.map((call) => [call.id, call]))
|
|
434
555
|
|
|
556
|
+
// A decision about some calls is not a decision about the rest. A call that
|
|
557
|
+
// already ran is answered from the record (`prior`); one that had not run and
|
|
558
|
+
// was not asked about is not run, and the model is told so.
|
|
559
|
+
if (plan.partial) {
|
|
560
|
+
for (const call of plan.response.message.toolCalls ?? []) {
|
|
561
|
+
if (prior?.has(call.id) || reviewedById.has(call.id) || denials.has(call.id)) continue
|
|
562
|
+
denials.set(call.id, NOT_RUN_BEFORE_RESTART)
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
435
566
|
// A gate denial belongs to the turn, not to the process that first evaluated
|
|
436
567
|
// it. Restore it before preparation so denied calls do not even reach a
|
|
437
568
|
// pre-tool hook after restart.
|
|
@@ -664,6 +664,18 @@ export interface ToolReviewRequest {
|
|
|
664
664
|
* in the session run without asking.
|
|
665
665
|
*/
|
|
666
666
|
readonly screenConsent?: true
|
|
667
|
+
/**
|
|
668
|
+
* Set when the request is the per-call review's question about one call (or
|
|
669
|
+
* the few asked together), not the whole batch. See `PerCallReviewConfig`.
|
|
670
|
+
*/
|
|
671
|
+
readonly perCall?: true
|
|
672
|
+
/**
|
|
673
|
+
* Aborts when the question stops being open: the turn was cancelled, a
|
|
674
|
+
* rule or the mode now covers the call, or an earlier refusal ended the
|
|
675
|
+
* turn. A host that shows the question closes it then; its answer is
|
|
676
|
+
* ignored. Present only with `perCall`.
|
|
677
|
+
*/
|
|
678
|
+
readonly signal?: AbortSignal
|
|
667
679
|
}
|
|
668
680
|
|
|
669
681
|
export type ToolReviewAnswer =
|
|
@@ -680,6 +692,12 @@ export type ToolReviewAnswer =
|
|
|
680
692
|
* cannot carry this. `note` is the person's own words.
|
|
681
693
|
*/
|
|
682
694
|
readonly declined?: ToolCallDeclined
|
|
695
|
+
/**
|
|
696
|
+
* The refusal also ends the turn: see `endTurn` on the `reject_tools`
|
|
697
|
+
* decision. A plain No sets it; a No that carries the person's reason
|
|
698
|
+
* leaves it out so the model reads the reason and carries on.
|
|
699
|
+
*/
|
|
700
|
+
readonly endTurn?: true
|
|
683
701
|
}
|
|
684
702
|
|
|
685
703
|
/** How a host asks a person. The one thing the kernel cannot supply. */
|
|
@@ -955,6 +973,8 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
955
973
|
turnId: request.turnId,
|
|
956
974
|
toolCalls: request.toolCalls,
|
|
957
975
|
...(screenConsent ? { screenConsent } : {}),
|
|
976
|
+
...(request.perCall ? { perCall: true as const } : {}),
|
|
977
|
+
...(request.perCall && context?.signal ? { signal: context.signal } : {}),
|
|
958
978
|
})
|
|
959
979
|
// The first look at the screen in a session is the operator's to allow:
|
|
960
980
|
// what is on it goes to the model provider, and a screenshot reads as
|
|
@@ -982,6 +1002,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
982
1002
|
action: 'reject_tools',
|
|
983
1003
|
feedback: answer.feedback ?? SCREEN_CONSENT_DECLINED_FEEDBACK,
|
|
984
1004
|
...(answer.declined ? { declined: answer.declined } : {}),
|
|
1005
|
+
...(answer.endTurn ? { endTurn: true as const } : {}),
|
|
985
1006
|
}
|
|
986
1007
|
}
|
|
987
1008
|
consent.sessions.add(sessionKey)
|
|
@@ -1038,6 +1059,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
1038
1059
|
action: 'reject_tools',
|
|
1039
1060
|
feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
|
|
1040
1061
|
...(answer.declined ? { declined: answer.declined } : {}),
|
|
1062
|
+
...(answer.endTurn ? { endTurn: true as const } : {}),
|
|
1041
1063
|
}
|
|
1042
1064
|
}
|
|
1043
1065
|
if (answer.kind === 'approve-all') remembered.all = true
|
|
@@ -1145,6 +1167,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
1145
1167
|
action: 'reject_tools',
|
|
1146
1168
|
feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
|
|
1147
1169
|
...(answer.declined ? { declined: answer.declined } : {}),
|
|
1170
|
+
...(answer.endTurn ? { endTurn: true as const } : {}),
|
|
1148
1171
|
}
|
|
1149
1172
|
}
|
|
1150
1173
|
// "Approve all" still latches for the calls that follow; it never
|
|
@@ -1164,6 +1187,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
1164
1187
|
action: 'reject_tools',
|
|
1165
1188
|
feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
|
|
1166
1189
|
...(answer.declined ? { declined: answer.declined } : {}),
|
|
1190
|
+
...(answer.endTurn ? { endTurn: true as const } : {}),
|
|
1167
1191
|
}
|
|
1168
1192
|
}
|
|
1169
1193
|
// Latches for the ordinary calls that follow, never for the next floor.
|
|
@@ -1182,6 +1206,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
1182
1206
|
action: 'reject_tools',
|
|
1183
1207
|
feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
|
|
1184
1208
|
...(answer.declined ? { declined: answer.declined } : {}),
|
|
1209
|
+
...(answer.endTurn ? { endTurn: true as const } : {}),
|
|
1185
1210
|
}
|
|
1186
1211
|
}
|
|
1187
1212
|
// Latches for the ordinary calls that follow, never for the next path.
|
|
@@ -1202,6 +1227,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
1202
1227
|
action: 'reject_tools',
|
|
1203
1228
|
feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
|
|
1204
1229
|
...(answer.declined ? { declined: answer.declined } : {}),
|
|
1230
|
+
...(answer.endTurn ? { endTurn: true as const } : {}),
|
|
1205
1231
|
}
|
|
1206
1232
|
}
|
|
1207
1233
|
if (answer.kind === 'approve-all') remembered.all = true
|
|
@@ -1222,6 +1248,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
1222
1248
|
action: 'reject_tools',
|
|
1223
1249
|
feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
|
|
1224
1250
|
...(answer.declined ? { declined: answer.declined } : {}),
|
|
1251
|
+
...(answer.endTurn ? { endTurn: true as const } : {}),
|
|
1225
1252
|
}
|
|
1226
1253
|
}
|
|
1227
1254
|
if (answer.kind === 'approve-all') remembered.all = true
|
|
@@ -1272,6 +1299,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
1272
1299
|
action: 'reject_tools',
|
|
1273
1300
|
feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
|
|
1274
1301
|
...(answer.declined ? { declined: answer.declined } : {}),
|
|
1302
|
+
...(answer.endTurn ? { endTurn: true as const } : {}),
|
|
1275
1303
|
}
|
|
1276
1304
|
}
|
|
1277
1305
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { createRuntimeContextMessage } from '../../types/message/index.js'
|
|
1
2
|
import type { Message, UserMessage } from '../../types/message/index.js'
|
|
3
|
+
import { NOTICE_TAG, frameNotice, readOperatorNotice } from '../../types/message/notice-frame.js'
|
|
2
4
|
import {
|
|
3
5
|
MONITOR_EVENT_CONTENT_MAX_CHARS,
|
|
4
6
|
MONITOR_NOTIFICATION_MAX_CHARS,
|
|
@@ -20,28 +22,24 @@ export function isOperatorUserMessage(message: Message): message is UserMessage
|
|
|
20
22
|
/**
|
|
21
23
|
* Guidance a host hands to a turn that is already running.
|
|
22
24
|
*
|
|
23
|
-
* This
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* This channel is distinct from `inboundMessages`. The manager injects its
|
|
26
|
+
* mailbox through that callback, and query-backed agents consume it at
|
|
27
|
+
* provider-valid message boundaries. Custom agents must consume their injected
|
|
28
|
+
* callback themselves. This channel instead lets a host attach guidance to a
|
|
29
|
+
* settled tool batch.
|
|
28
30
|
*
|
|
29
|
-
* **
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* `tool_result`, and `executor.ts` notes that this is also what makes a
|
|
36
|
-
* rejection *steer*, because the model reads it in the slot it already
|
|
37
|
-
* attends to for tool outcomes. Steering is the same delivery with the
|
|
38
|
-
* refusal removed.
|
|
31
|
+
* **Where the text lands.** A `tool_use` block must be answered by a
|
|
32
|
+
* `tool_result` with the same id, so nothing can sit between them. The
|
|
33
|
+
* guidance therefore follows the finished batch as a user-role message of its
|
|
34
|
+
* own, at the next request boundary, framed as the operator's words. It is not
|
|
35
|
+
* written into a tool result: a tool result carries only what the tool
|
|
36
|
+
* returned, and a model reads instructions placed inside one as tool output.
|
|
39
37
|
*
|
|
40
|
-
* It does not interrupt. The batch in flight
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
38
|
+
* It does not interrupt. The batch in flight finishes, and the guidance lands
|
|
39
|
+
* where the model looks next. A host that wants the current work stopped wants
|
|
40
|
+
* `AbortSignal`, which is a different question with a different answer, and
|
|
41
|
+
* conflating the two is how "please also check the tests" ends up killing a
|
|
42
|
+
* half-written file.
|
|
45
43
|
*/
|
|
46
44
|
export interface SteeringChannel {
|
|
47
45
|
/**
|
|
@@ -53,7 +51,7 @@ export interface SteeringChannel {
|
|
|
53
51
|
* discards a host's instruction.
|
|
54
52
|
*
|
|
55
53
|
* Empty and whitespace-only text is ignored, so a stray keystroke does
|
|
56
|
-
* not
|
|
54
|
+
* not send a blank message.
|
|
57
55
|
*/
|
|
58
56
|
steer(text: string): void
|
|
59
57
|
|
|
@@ -85,60 +83,9 @@ export class SteeringBinding implements SteeringChannel {
|
|
|
85
83
|
}
|
|
86
84
|
}
|
|
87
85
|
|
|
88
|
-
/**
|
|
89
|
-
* The frame the guidance arrives in.
|
|
90
|
-
*
|
|
91
|
-
* Labelled because the model is being handed text from a party other than the
|
|
92
|
-
* tool whose result it is reading, in that tool's slot. Unlabelled, it reads
|
|
93
|
-
* as something the tool said — so a steer saying "stop and ask me first" would
|
|
94
|
-
* look like output from `bash`.
|
|
95
|
-
*
|
|
96
|
-
* This is NOT the untrusted-content envelope. The host operating the turn is
|
|
97
|
-
* the one party whose words the agent SHOULD act on; framing them as material
|
|
98
|
-
* to be worked with rather than followed would inverting the very thing the
|
|
99
|
-
* host is trying to do. Different party, different frame, on purpose.
|
|
100
|
-
*/
|
|
101
|
-
/** The same slot as steering, for notices the kernel itself has to deliver. */
|
|
102
|
-
export function attachNotice(
|
|
103
|
-
messages: readonly Message[],
|
|
104
|
-
channel: SteeringChannel | undefined,
|
|
105
|
-
format: (text: string) => string,
|
|
106
|
-
/**
|
|
107
|
-
* Told only when the text actually landed on a result the model will read.
|
|
108
|
-
*
|
|
109
|
-
* Not on the two paths above it: a batch with no tool result leaves the
|
|
110
|
-
* notice queued, and a result whose content is not a string puts it back.
|
|
111
|
-
* Draining is therefore not delivery, which matters to the caller that
|
|
112
|
-
* keeps its own record of what the text accounts for — `AwaitedJobs`
|
|
113
|
-
* drops an exit's entry here, and a premature drop would strand the exit.
|
|
114
|
-
*/
|
|
115
|
-
onDelivered?: () => void,
|
|
116
|
-
): readonly Message[] {
|
|
117
|
-
if (!channel?.pending) return messages
|
|
118
|
-
let lastToolIndex = -1
|
|
119
|
-
for (let index = messages.length - 1; index >= 0; index--) {
|
|
120
|
-
if (messages[index]?.role === 'tool') {
|
|
121
|
-
lastToolIndex = index
|
|
122
|
-
break
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (lastToolIndex === -1) return messages
|
|
126
|
-
const text = channel.drain()
|
|
127
|
-
if (text === undefined) return messages
|
|
128
|
-
const target = messages[lastToolIndex] as Message
|
|
129
|
-
if (typeof target.content !== 'string') {
|
|
130
|
-
channel.steer(text)
|
|
131
|
-
return messages
|
|
132
|
-
}
|
|
133
|
-
const next = [...messages]
|
|
134
|
-
next[lastToolIndex] = { ...target, content: target.content + format(text) }
|
|
135
|
-
onDelivered?.()
|
|
136
|
-
return next
|
|
137
|
-
}
|
|
138
|
-
|
|
139
86
|
/** What the model reads when a background job it started has ended. */
|
|
140
87
|
export function formatJobNote(text: string): string {
|
|
141
|
-
return
|
|
88
|
+
return frameNotice('job', '[Background job update]', text)
|
|
142
89
|
}
|
|
143
90
|
|
|
144
91
|
/**
|
|
@@ -154,7 +101,7 @@ export function formatJobNote(text: string): string {
|
|
|
154
101
|
* piled up while the model was busy.
|
|
155
102
|
*/
|
|
156
103
|
export function formatMonitorNote(text: string): string {
|
|
157
|
-
return
|
|
104
|
+
return frameNotice('monitor', '[Monitor update]', capMonitorNotifications(text))
|
|
158
105
|
}
|
|
159
106
|
|
|
160
107
|
/** One `formatSystemEvent` rendering, found by its nonce-bound opening and closing tags. */
|
|
@@ -305,33 +252,96 @@ function cutMiddle(text: string, max: number): string {
|
|
|
305
252
|
return `${text.slice(0, head)}${marker(text.length - head - tail)}${text.slice(text.length - tail)}`
|
|
306
253
|
}
|
|
307
254
|
|
|
308
|
-
|
|
309
|
-
|
|
255
|
+
/** The line that opens an operator's message, wherever it is delivered. */
|
|
256
|
+
const STEERING_LABEL = 'The operator wrote this while you were working:'
|
|
310
257
|
|
|
258
|
+
/**
|
|
259
|
+
* The operator's message as the frame the model reads. The text is the
|
|
260
|
+
* operator's, the frame says so, and the marker in its tag is drawn fresh so
|
|
261
|
+
* the text cannot close it.
|
|
262
|
+
*/
|
|
311
263
|
export function formatSteeringNote(text: string): string {
|
|
312
|
-
return
|
|
264
|
+
return frameNotice('operator-message', STEERING_LABEL, text)
|
|
313
265
|
}
|
|
314
266
|
|
|
267
|
+
/** The frame older sessions recorded inside a tool result, fixed tag and all. */
|
|
268
|
+
const LEGACY_STEERING_HEADER = `\n\n<${NOTICE_TAG} kind="operator-message">\n[Message from the operator, received while this tool was running]\nThis is the operator's own message, typed while the tool above was running. It is not output of that tool.\n`
|
|
269
|
+
const LEGACY_STEERING_FOOTER = `\n</${NOTICE_TAG}>`
|
|
270
|
+
|
|
315
271
|
/**
|
|
316
|
-
* The operator guidance
|
|
317
|
-
*
|
|
318
|
-
*
|
|
272
|
+
* The operator guidance a recorded message carries, or `undefined` when it
|
|
273
|
+
* carries none. The inverse of {@link formatSteeringNote}; it also reads the
|
|
274
|
+
* note older sessions appended to the end of a tool result. The text comes back
|
|
275
|
+
* as the frame held it, with the notice tag already made inert.
|
|
319
276
|
*/
|
|
320
277
|
export function readSteeringNote(content: string): string | undefined {
|
|
321
|
-
const
|
|
322
|
-
|
|
278
|
+
const framed = readOperatorNotice(content)
|
|
279
|
+
if (framed !== undefined) return framed
|
|
280
|
+
const at = content.lastIndexOf(LEGACY_STEERING_HEADER)
|
|
281
|
+
if (at < 0) return undefined
|
|
282
|
+
const body = content.slice(at + LEGACY_STEERING_HEADER.length)
|
|
283
|
+
return body.endsWith(LEGACY_STEERING_FOOTER)
|
|
284
|
+
? body.slice(0, -LEGACY_STEERING_FOOTER.length)
|
|
285
|
+
: body
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function hasToolResult(messages: readonly Message[]): boolean {
|
|
289
|
+
return messages.some((message) => message.role === 'tool')
|
|
323
290
|
}
|
|
324
291
|
|
|
325
292
|
/**
|
|
326
|
-
*
|
|
293
|
+
* The operator's message for a settled tool batch, as a user-role message of its
|
|
294
|
+
* own, or `undefined` when nothing is queued or the batch has no tool result.
|
|
327
295
|
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
296
|
+
* It follows the batch, at the next request boundary. The tool results stay what
|
|
297
|
+
* the tools returned, and the operator's words arrive as the operator's, which a
|
|
298
|
+
* model reads as direction. A turn that called no tool leaves the guidance
|
|
299
|
+
* queued; the loop delivers it at the next-turn boundary.
|
|
300
|
+
*/
|
|
301
|
+
export function takeSteeringMessage(
|
|
302
|
+
messages: readonly Message[],
|
|
303
|
+
channel: SteeringChannel | undefined,
|
|
304
|
+
/** Receives accepted operator text, never inferred from a tool's output. */
|
|
305
|
+
onDelivered?: (text: string) => void,
|
|
306
|
+
): UserMessage | undefined {
|
|
307
|
+
if (!channel?.pending || !hasToolResult(messages)) return undefined
|
|
308
|
+
const guidance = channel.drain()
|
|
309
|
+
if (guidance === undefined) return undefined
|
|
310
|
+
onDelivered?.(guidance)
|
|
311
|
+
return createRuntimeContextMessage(formatSteeringNote(guidance), 'steering')
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* A kernel notice (a job's exit, a monitor's events) for a settled tool batch,
|
|
316
|
+
* as a user-role message of its own, under the same rules as
|
|
317
|
+
* {@link takeSteeringMessage}.
|
|
318
|
+
*/
|
|
319
|
+
export function takeNoticeMessage(
|
|
320
|
+
messages: readonly Message[],
|
|
321
|
+
channel: SteeringChannel | undefined,
|
|
322
|
+
format: (text: string) => string,
|
|
323
|
+
/**
|
|
324
|
+
* Told only when the text actually became a message the model will read.
|
|
325
|
+
* Draining is not delivery, which matters to the caller that keeps its own
|
|
326
|
+
* record of what the text accounts for: `AwaitedJobs` drops an exit's entry
|
|
327
|
+
* here, and a premature drop would strand the exit.
|
|
328
|
+
*/
|
|
329
|
+
onDelivered?: () => void,
|
|
330
|
+
): UserMessage | undefined {
|
|
331
|
+
if (!channel?.pending || !hasToolResult(messages)) return undefined
|
|
332
|
+
const text = channel.drain()
|
|
333
|
+
if (text === undefined) return undefined
|
|
334
|
+
onDelivered?.()
|
|
335
|
+
return createRuntimeContextMessage(format(text), 'job-exit')
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Append the guidance to the last tool result in a settled batch.
|
|
330
340
|
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
341
|
+
* @deprecated The kernel no longer delivers guidance this way: a tool result
|
|
342
|
+
* carries only what the tool returned, and {@link takeSteeringMessage} sends the
|
|
343
|
+
* operator's words as a message of their own. Kept so a host that calls it keeps
|
|
344
|
+
* working.
|
|
335
345
|
*/
|
|
336
346
|
export function attachSteering(
|
|
337
347
|
messages: readonly Message[],
|
|
@@ -354,12 +364,6 @@ export function attachSteering(
|
|
|
354
364
|
if (guidance === undefined) return messages
|
|
355
365
|
|
|
356
366
|
const target = messages[lastToolIndex] as Message
|
|
357
|
-
// Only text is extended. A tool that answered with structured content —
|
|
358
|
-
// an image block, say — has a shape the model reads positionally, and
|
|
359
|
-
// appending a string to it would either be dropped or corrupt the block.
|
|
360
|
-
// Such a result keeps its content and the note follows it as its own text
|
|
361
|
-
// part where the shape allows, and otherwise the guidance stays queued
|
|
362
|
-
// for the next turn rather than being forced into a slot it does not fit.
|
|
363
367
|
if (typeof target.content !== 'string') {
|
|
364
368
|
channel.steer(guidance)
|
|
365
369
|
return messages
|
|
@@ -368,7 +372,7 @@ export function attachSteering(
|
|
|
368
372
|
const next = [...messages]
|
|
369
373
|
next[lastToolIndex] = {
|
|
370
374
|
...target,
|
|
371
|
-
content: target.content
|
|
375
|
+
content: `${target.content}\n\n${formatSteeringNote(guidance)}`,
|
|
372
376
|
}
|
|
373
377
|
onDelivered?.(guidance)
|
|
374
378
|
return next
|
package/src/store/budget/disk.ts
CHANGED
|
@@ -238,6 +238,9 @@ function usageAtLeast(current: TokenUsage | undefined, previous: TokenUsage | un
|
|
|
238
238
|
if (current === undefined) return false
|
|
239
239
|
return Object.entries(previous).every(
|
|
240
240
|
([field, amount]) =>
|
|
241
|
-
typeof amount !== 'number' ||
|
|
241
|
+
typeof amount !== 'number' ||
|
|
242
|
+
field === 'cacheTtlMs' ||
|
|
243
|
+
// `cacheTtlMs` is a per-call lifetime, not a running count: it has no order.
|
|
244
|
+
((current as unknown as Record<string, number | undefined>)[field] ?? 0) >= amount,
|
|
242
245
|
)
|
|
243
246
|
}
|
package/src/tools/admit-facts.ts
CHANGED
|
@@ -10,6 +10,11 @@ const FAILURE_REASONS: readonly ToolFailureReason[] = [
|
|
|
10
10
|
'unknown_tool',
|
|
11
11
|
'timed_out',
|
|
12
12
|
'invalid_input',
|
|
13
|
+
'not_read',
|
|
14
|
+
'changed_on_disk',
|
|
15
|
+
'text_not_found',
|
|
16
|
+
'not_unique',
|
|
17
|
+
'invalid_arguments',
|
|
13
18
|
]
|
|
14
19
|
|
|
15
20
|
const WRITE_OPERATIONS = ['created', 'updated', 'unchanged', 'wrote'] as const
|
|
@@ -105,6 +110,7 @@ export function admitToolFacts(value: unknown): ToolFacts | undefined {
|
|
|
105
110
|
typeof value.exitCode === 'number' && Number.isSafeInteger(value.exitCode)
|
|
106
111
|
? value.exitCode
|
|
107
112
|
: undefined,
|
|
113
|
+
exitNote: text(value.exitNote),
|
|
108
114
|
timedOut: flag(value.timedOut),
|
|
109
115
|
signal: text(value.signal),
|
|
110
116
|
binary:
|