@namzu/sdk 56.2.0 → 57.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 +37 -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/agents/index.d.ts +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +1 -0
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/plan.d.ts +20 -0
- package/dist/agents/plan.d.ts.map +1 -0
- package/dist/agents/plan.js +27 -0
- package/dist/agents/plan.js.map +1 -0
- package/dist/compaction/dangling.d.ts +12 -1
- package/dist/compaction/dangling.d.ts.map +1 -1
- package/dist/compaction/dangling.js +17 -3
- package/dist/compaction/dangling.js.map +1 -1
- package/dist/compaction/index.d.ts +2 -2
- package/dist/compaction/index.d.ts.map +1 -1
- package/dist/compaction/index.js.map +1 -1
- package/dist/compaction/manager.d.ts +16 -0
- package/dist/compaction/manager.d.ts.map +1 -1
- package/dist/compaction/manager.js +55 -0
- package/dist/compaction/manager.js.map +1 -1
- package/dist/compaction/manual.d.ts +2 -0
- package/dist/compaction/manual.d.ts.map +1 -1
- package/dist/compaction/manual.js +33 -4
- package/dist/compaction/manual.js.map +1 -1
- package/dist/compaction/restore.d.ts +51 -0
- package/dist/compaction/restore.d.ts.map +1 -0
- package/dist/compaction/restore.js +123 -0
- package/dist/compaction/restore.js.map +1 -0
- package/dist/compaction/types.d.ts +22 -0
- package/dist/compaction/types.d.ts.map +1 -1
- package/dist/compaction/wire.d.ts +3 -1
- package/dist/compaction/wire.d.ts.map +1 -1
- package/dist/compaction/wire.js +16 -0
- package/dist/compaction/wire.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/plugin/shell-hook.d.ts +19 -1
- package/dist/plugin/shell-hook.d.ts.map +1 -1
- package/dist/plugin/shell-hook.js +21 -5
- package/dist/plugin/shell-hook.js.map +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts +2 -2
- package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
- package/dist/prompt/coding-agent-doctrine.js +3 -2
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/provider/errors.d.ts +3 -0
- package/dist/provider/errors.d.ts.map +1 -1
- package/dist/provider/errors.js +23 -3
- package/dist/provider/errors.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +34 -3
- package/dist/provider/fallback.js.map +1 -1
- package/dist/provider/retry.d.ts +12 -5
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +22 -8
- package/dist/provider/retry.js.map +1 -1
- package/dist/public-runtime.d.ts +4 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +4 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +1 -1
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.d.ts +42 -0
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +77 -0
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/query/calls-that-never-started.d.ts +12 -0
- package/dist/runtime/query/calls-that-never-started.d.ts.map +1 -0
- package/dist/runtime/query/calls-that-never-started.js +41 -0
- package/dist/runtime/query/calls-that-never-started.js.map +1 -0
- package/dist/runtime/query/executor/tool-call-admission.d.ts +2 -2
- package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
- package/dist/runtime/query/executor/tool-call-admission.js +13 -9
- package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +8 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +35 -0
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/file-evidence-context.js +1 -1
- package/dist/runtime/query/file-evidence-context.js.map +1 -1
- package/dist/runtime/query/index.d.ts +6 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +5 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +20 -0
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +45 -25
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
- package/dist/runtime/query/prepare-turn.js +10 -1
- package/dist/runtime/query/prepare-turn.js.map +1 -1
- package/dist/scheduler/completion-inbox.js +3 -3
- package/dist/scheduler/completion-inbox.js.map +1 -1
- package/dist/tools/admit-facts.d.ts.map +1 -1
- package/dist/tools/admit-facts.js +1 -0
- package/dist/tools/admit-facts.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts +42 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +115 -48
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +55 -5
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/file-read-tracker.d.ts.map +1 -1
- package/dist/tools/file-read-tracker.js +28 -0
- package/dist/tools/file-read-tracker.js.map +1 -1
- package/dist/types/agent/base.d.ts +6 -0
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/errors/catalog.d.ts +3 -0
- package/dist/types/errors/catalog.d.ts.map +1 -1
- package/dist/types/errors/catalog.js +24 -2
- package/dist/types/errors/catalog.js.map +1 -1
- package/dist/types/provider/error.d.ts +7 -0
- package/dist/types/provider/error.d.ts.map +1 -1
- package/dist/types/provider/errors.d.ts +24 -0
- package/dist/types/provider/errors.d.ts.map +1 -1
- package/dist/types/provider/errors.js +101 -1
- package/dist/types/provider/errors.js.map +1 -1
- package/dist/types/provider/stream.d.ts +6 -0
- package/dist/types/provider/stream.d.ts.map +1 -1
- package/dist/types/session/events.d.ts +8 -0
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js.map +1 -1
- package/dist/types/tool/index.d.ts +39 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/tool/presentation.d.ts +4 -0
- package/dist/types/tool/presentation.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/agents/QueryAgent.ts +1 -0
- package/src/agents/index.ts +1 -0
- package/src/agents/plan.ts +30 -0
- package/src/compaction/dangling.ts +35 -3
- package/src/compaction/index.ts +4 -0
- package/src/compaction/manager.ts +53 -0
- package/src/compaction/manual.ts +42 -10
- package/src/compaction/restore.ts +159 -0
- package/src/compaction/types.ts +25 -0
- package/src/compaction/wire.ts +26 -1
- package/src/index.ts +2 -0
- package/src/plugin/shell-hook.ts +33 -5
- package/src/prompt/coding-agent-doctrine.ts +3 -2
- package/src/provider/errors.ts +27 -3
- package/src/provider/fallback.ts +34 -2
- package/src/provider/retry.ts +30 -10
- package/src/public-runtime.ts +8 -0
- package/src/public-types.ts +3 -0
- package/src/runtime/jobs/registry.ts +105 -0
- package/src/runtime/query/calls-that-never-started.ts +37 -0
- package/src/runtime/query/executor/tool-call-admission.ts +12 -9
- package/src/runtime/query/executor.ts +37 -0
- package/src/runtime/query/file-evidence-context.ts +1 -1
- package/src/runtime/query/index.ts +12 -0
- package/src/runtime/query/iteration/phases/compaction.ts +19 -0
- package/src/runtime/query/iteration/phases/context.ts +3 -0
- package/src/runtime/query/iteration/stream-turn.ts +47 -25
- package/src/runtime/query/prepare-turn.ts +10 -1
- package/src/scheduler/completion-inbox.ts +3 -3
- package/src/tools/admit-facts.ts +1 -0
- package/src/tools/builtins/bash.ts +149 -54
- package/src/tools/builtins/read-file.ts +66 -4
- package/src/tools/file-read-tracker.ts +25 -1
- package/src/types/agent/base.ts +7 -0
- package/src/types/errors/catalog.ts +31 -2
- package/src/types/provider/error.ts +7 -0
- package/src/types/provider/errors.ts +120 -1
- package/src/types/provider/stream.ts +6 -0
- package/src/types/session/events.ts +8 -0
- package/src/types/tool/index.ts +40 -0
- package/src/types/tool/presentation.ts +4 -0
|
@@ -6,6 +6,7 @@ import { unknownProgramInLine } from '../../authorization/program.js'
|
|
|
6
6
|
import { type SkillGrantSet, compileSkillGrant } from '../../authorization/skill-grant.js'
|
|
7
7
|
import { extractFromToolCall, extractFromToolResult } from '../../compaction/extractor.js'
|
|
8
8
|
import type { WorkingStateManager } from '../../compaction/manager.js'
|
|
9
|
+
import { recordRestorable } from '../../compaction/restore.js'
|
|
9
10
|
import { GENAI, NAMZU } from '../../constants/telemetry/index.js'
|
|
10
11
|
import type { PluginLifecycleManager } from '../../plugin/lifecycle.js'
|
|
11
12
|
import { buildProbeContext } from '../../probe/context.js'
|
|
@@ -13,6 +14,7 @@ import { ProbeVetoError } from '../../probe/errors.js'
|
|
|
13
14
|
import { probe as defaultProbeRegistry } from '../../probe/registry.js'
|
|
14
15
|
import type { ProbeEnforcement } from '../../probe/registry.js'
|
|
15
16
|
import type { ActivityStore } from '../../store/activity/memory.js'
|
|
17
|
+
import { fingerprintContent } from '../../tools/builtins/content-fingerprint.js'
|
|
16
18
|
import { sandboxShellSpawn, withoutBashStartup } from '../../tools/command-shell.js'
|
|
17
19
|
import { isAlwaysDestructive } from '../../tools/defineTool.js'
|
|
18
20
|
import { createFileReadTracker } from '../../tools/file-read-tracker.js'
|
|
@@ -700,6 +702,7 @@ export class ToolExecutor {
|
|
|
700
702
|
private emitEvent: EmitEvent
|
|
701
703
|
private log: Logger
|
|
702
704
|
private workingStateManager?: WorkingStateManager
|
|
705
|
+
private historyView?: () => readonly Message[]
|
|
703
706
|
private probes: ProbeEnforcement
|
|
704
707
|
private parentSpan?: Span
|
|
705
708
|
private readonly toolCallBudget?: ToolCallBudget
|
|
@@ -749,6 +752,30 @@ export class ToolExecutor {
|
|
|
749
752
|
this.workingStateManager = manager
|
|
750
753
|
}
|
|
751
754
|
|
|
755
|
+
/**
|
|
756
|
+
* Read the conversation the model is shown, as it is now. Lets a tool ask
|
|
757
|
+
* whether an earlier receipt is still there (see `ToolContext.receiptVisible`)
|
|
758
|
+
* without being handed the history.
|
|
759
|
+
*/
|
|
760
|
+
setHistoryView(view: () => readonly Message[]): void {
|
|
761
|
+
this.historyView = view
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
private receiptVisible = (callId: string, renderedFingerprint: string): boolean => {
|
|
765
|
+
const history = this.historyView?.()
|
|
766
|
+
if (!history) return false
|
|
767
|
+
for (let index = history.length - 1; index >= 0; index -= 1) {
|
|
768
|
+
const message = history[index]
|
|
769
|
+
if (message?.role !== 'tool' || message.toolCallId !== callId) continue
|
|
770
|
+
return (
|
|
771
|
+
typeof message.content === 'string' &&
|
|
772
|
+
message.isError !== true &&
|
|
773
|
+
fingerprintContent(message.content) === renderedFingerprint
|
|
774
|
+
)
|
|
775
|
+
}
|
|
776
|
+
return false
|
|
777
|
+
}
|
|
778
|
+
|
|
752
779
|
setSandbox(sandbox: Sandbox): void {
|
|
753
780
|
this.config = { ...this.config, sandbox }
|
|
754
781
|
this.sandboxGate = undefined
|
|
@@ -2185,6 +2212,7 @@ export class ToolExecutor {
|
|
|
2185
2212
|
this.dispatchNested(name, input, context, recordObservation, undefined, options),
|
|
2186
2213
|
sandbox: this.config.sandbox,
|
|
2187
2214
|
fileReadTracker: this.fileReadTracker,
|
|
2215
|
+
receiptVisible: this.receiptVisible,
|
|
2188
2216
|
...(this.config.codeAnalysis ? { codeAnalysis: this.config.codeAnalysis } : {}),
|
|
2189
2217
|
// Bound to this turn, once. Binding here rather than passing the
|
|
2190
2218
|
// owner from the tool is what makes the scoping structural: there
|
|
@@ -2584,6 +2612,15 @@ export class ToolExecutor {
|
|
|
2584
2612
|
|
|
2585
2613
|
if (this.workingStateManager) {
|
|
2586
2614
|
extractFromToolResult(this.workingStateManager, toolName, output, effectiveIsError)
|
|
2615
|
+
// What the model was shown, kept whole so a compaction can put it back.
|
|
2616
|
+
recordRestorable(this.workingStateManager, {
|
|
2617
|
+
toolName,
|
|
2618
|
+
input,
|
|
2619
|
+
output: typeof modelContent === 'string' ? modelContent : output,
|
|
2620
|
+
success: !effectiveIsError,
|
|
2621
|
+
unchanged: result.facts?.kind === 'read' && result.facts.unchanged === true,
|
|
2622
|
+
workingDirectory: this.config.workingDirectory,
|
|
2623
|
+
})
|
|
2587
2624
|
for (const pin of this.config.abortSignal.aborted ? [] : (result.workingState ?? [])) {
|
|
2588
2625
|
this.workingStateManager.pin(pin.key, pin.text, toolName)
|
|
2589
2626
|
}
|
|
@@ -149,7 +149,7 @@ export function recordFileEvidence(
|
|
|
149
149
|
const lead =
|
|
150
150
|
recorded.length === 0
|
|
151
151
|
? `${FILE_EVIDENCE_HEADING}: ${FILE_EVIDENCE_GUIDANCE}`
|
|
152
|
-
: `${FILE_EVIDENCE_HEADING}, changed since the last entry. An entry for a path replaces the earlier entries for that path.`
|
|
152
|
+
: `${FILE_EVIDENCE_HEADING}, changed since the last entry; \`bodyInCall\` and \`observedFingerprint\` read as before. An entry for a path replaces the earlier entries for that path.`
|
|
153
153
|
return [
|
|
154
154
|
lead,
|
|
155
155
|
...(gone.length > 0 ? [`${NO_LONGER_HOLDS}${JSON.stringify(gone)}`] : []),
|
|
@@ -814,6 +814,13 @@ export interface QueryParams {
|
|
|
814
814
|
*/
|
|
815
815
|
projectInstructionContext?: ProjectInstructionContext
|
|
816
816
|
|
|
817
|
+
/**
|
|
818
|
+
* Text the host wants carried across a compaction, such as where its plan
|
|
819
|
+
* file is. Asked once per pass, after the summary is written; the answer is
|
|
820
|
+
* appended to the restored context. Returning nothing adds nothing.
|
|
821
|
+
*/
|
|
822
|
+
postCompactionContext?: () => string | undefined | Promise<string | undefined>
|
|
823
|
+
|
|
817
824
|
/**
|
|
818
825
|
* Where this conversation's durable state lives.
|
|
819
826
|
*
|
|
@@ -1656,6 +1663,8 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
1656
1663
|
const compactionConfig = params.compactionConfig
|
|
1657
1664
|
? CompactionConfigSchema.parse(params.compactionConfig)
|
|
1658
1665
|
: undefined
|
|
1666
|
+
// The live history, so a tool can ask whether an earlier receipt is still in it.
|
|
1667
|
+
toolExecutor.setHistoryView(() => ctx.recorder.messages)
|
|
1659
1668
|
if (compactionConfig && compactionConfig.strategy !== 'disabled') {
|
|
1660
1669
|
workingStateManager = new WorkingStateManager(compactionConfig)
|
|
1661
1670
|
toolExecutor.setWorkingStateManager(workingStateManager)
|
|
@@ -1829,6 +1838,9 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
1829
1838
|
...(params.projectInstructionContext
|
|
1830
1839
|
? { projectInstructionContext: params.projectInstructionContext }
|
|
1831
1840
|
: {}),
|
|
1841
|
+
...(params.postCompactionContext
|
|
1842
|
+
? { postCompactionContext: params.postCompactionContext }
|
|
1843
|
+
: {}),
|
|
1832
1844
|
...(providerContextWindow !== undefined ? { providerContextWindow } : {}),
|
|
1833
1845
|
resolveModelContextWindow: async (model) => {
|
|
1834
1846
|
if (!modelContextWindows.has(model)) {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '../../../../compaction/plan.js'
|
|
12
12
|
import type { ContextReducer, ContextReduction } from '../../../../compaction/reducer.js'
|
|
13
13
|
import { createSlidingWindowReducer } from '../../../../compaction/reducer.js'
|
|
14
|
+
import { renderRestoredContext } from '../../../../compaction/restore.js'
|
|
14
15
|
import { findRetainedIndices } from '../../../../compaction/retention.js'
|
|
15
16
|
import { serializeState } from '../../../../compaction/serializer.js'
|
|
16
17
|
import {
|
|
@@ -1003,6 +1004,24 @@ async function runCompactionCheckInner(
|
|
|
1003
1004
|
compactedContent = serializeState(manager.getState())
|
|
1004
1005
|
}
|
|
1005
1006
|
|
|
1007
|
+
// Skills the model loaded and files it read go back verbatim after the
|
|
1008
|
+
// summary, with whatever the host wants carried across (its plan file).
|
|
1009
|
+
let hostNote: string | undefined
|
|
1010
|
+
try {
|
|
1011
|
+
hostNote = await ctx.postCompactionContext?.()
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
// A host note is an addition; the pass does not depend on it.
|
|
1014
|
+
ctx.log.warn('The host note for a compaction could not be read — leaving it out', {
|
|
1015
|
+
[NAMZU.TURN_ID]: ctx.recorder.turnId,
|
|
1016
|
+
'exception.message': error instanceof Error ? error.message : String(error),
|
|
1017
|
+
})
|
|
1018
|
+
}
|
|
1019
|
+
const restored = renderRestoredContext(manager.getState(), {
|
|
1020
|
+
contextWindowTokens: budget,
|
|
1021
|
+
...(hostNote ? { hostNote } : {}),
|
|
1022
|
+
})
|
|
1023
|
+
if (restored) compactedContent = `${compactedContent}\n\n${restored}`
|
|
1024
|
+
|
|
1006
1025
|
// The pointer is only worth a line when both tools it names are mounted.
|
|
1007
1026
|
const mounted = new Set(ctx.tools?.listNames?.() ?? [])
|
|
1008
1027
|
const compactionMessage = buildCompactionMessage(
|
|
@@ -256,6 +256,9 @@ export interface IterationContext {
|
|
|
256
256
|
/** Live project policy; separate from human inbound continuation. */
|
|
257
257
|
readonly projectInstructionContext?: ProjectInstructionContext
|
|
258
258
|
|
|
259
|
+
/** Host text appended to the restored context after a compaction. */
|
|
260
|
+
readonly postCompactionContext?: () => string | undefined | Promise<string | undefined>
|
|
261
|
+
|
|
259
262
|
readonly workingStateManager?: WorkingStateManager
|
|
260
263
|
|
|
261
264
|
/**
|
|
@@ -30,7 +30,7 @@ import type {
|
|
|
30
30
|
ReasoningBlock,
|
|
31
31
|
ToolInputError,
|
|
32
32
|
} from '../../../types/message/index.js'
|
|
33
|
-
import { ProviderError } from '../../../types/provider/errors.js'
|
|
33
|
+
import { ProviderError, classifyProviderError } from '../../../types/provider/errors.js'
|
|
34
34
|
import type {
|
|
35
35
|
ChatCompletionResponse,
|
|
36
36
|
LLMProvider,
|
|
@@ -298,7 +298,7 @@ export async function* streamProviderTurn(
|
|
|
298
298
|
let streamedLength = 0
|
|
299
299
|
const toolBuckets = new Map<number, ToolCallBucket>()
|
|
300
300
|
// Places a fragment that came without an index; see `ToolCallIndexer`.
|
|
301
|
-
|
|
301
|
+
let toolIndexer = new ToolCallIndexer()
|
|
302
302
|
// The call the model's latest output went to, or `undefined` once text,
|
|
303
303
|
// reasoning or a hosted tool followed it. An output limit, a content
|
|
304
304
|
// filter or a dropped stream stops the response wherever it is, so this
|
|
@@ -397,6 +397,36 @@ export async function* streamProviderTurn(
|
|
|
397
397
|
})
|
|
398
398
|
: undefined
|
|
399
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Drop everything the failed attempt wrote, so the request starts over under
|
|
402
|
+
* the same message: after a restarting retry, or a swap that says the same.
|
|
403
|
+
* The failed attempt's text, reasoning and tool calls are void. A call is
|
|
404
|
+
* restarted only while still being written and nothing runs before the
|
|
405
|
+
* response settles, so a half-written call never reaches the model.
|
|
406
|
+
*/
|
|
407
|
+
const discardAttempt = () => {
|
|
408
|
+
toolBuckets.clear()
|
|
409
|
+
toolIndexer = new ToolCallIndexer()
|
|
410
|
+
framingViolated = false
|
|
411
|
+
text = new StreamTextAccumulator()
|
|
412
|
+
reasoningBuckets.clear()
|
|
413
|
+
citations.length = 0
|
|
414
|
+
replayState = undefined
|
|
415
|
+
id = ''
|
|
416
|
+
finishReason = 'stop'
|
|
417
|
+
reportedFinishReason = undefined
|
|
418
|
+
finishDetail = undefined
|
|
419
|
+
streamedLength = 0
|
|
420
|
+
lastOutputCall = undefined
|
|
421
|
+
usage = {
|
|
422
|
+
promptTokens: 0,
|
|
423
|
+
completionTokens: 0,
|
|
424
|
+
totalTokens: 0,
|
|
425
|
+
cachedTokens: 0,
|
|
426
|
+
cacheWriteTokens: 0,
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
400
430
|
try {
|
|
401
431
|
for (;;) {
|
|
402
432
|
const next = it.next()
|
|
@@ -426,28 +456,7 @@ export async function* streamProviderTurn(
|
|
|
426
456
|
...(chunk.retry.restart ? { restart: true } : {}),
|
|
427
457
|
})
|
|
428
458
|
yield* drainPending()
|
|
429
|
-
if (chunk.retry.restart)
|
|
430
|
-
// The failed attempt's text and reasoning are void: the request
|
|
431
|
-
// starts over under the same message. The decorator never
|
|
432
|
-
// restarts once a tool call began, so no bucket is left behind.
|
|
433
|
-
text = new StreamTextAccumulator()
|
|
434
|
-
reasoningBuckets.clear()
|
|
435
|
-
citations.length = 0
|
|
436
|
-
replayState = undefined
|
|
437
|
-
id = ''
|
|
438
|
-
finishReason = 'stop'
|
|
439
|
-
reportedFinishReason = undefined
|
|
440
|
-
finishDetail = undefined
|
|
441
|
-
streamedLength = 0
|
|
442
|
-
lastOutputCall = undefined
|
|
443
|
-
usage = {
|
|
444
|
-
promptTokens: 0,
|
|
445
|
-
completionTokens: 0,
|
|
446
|
-
totalTokens: 0,
|
|
447
|
-
cachedTokens: 0,
|
|
448
|
-
cacheWriteTokens: 0,
|
|
449
|
-
}
|
|
450
|
-
}
|
|
459
|
+
if (chunk.retry.restart) discardAttempt()
|
|
451
460
|
continue
|
|
452
461
|
}
|
|
453
462
|
|
|
@@ -472,8 +481,10 @@ export async function* streamProviderTurn(
|
|
|
472
481
|
code: chunk.fallback.code,
|
|
473
482
|
...(chunk.fallback.status !== undefined ? { status: chunk.fallback.status } : {}),
|
|
474
483
|
reason: chunk.fallback.reason,
|
|
484
|
+
...(chunk.fallback.restart ? { restart: true } : {}),
|
|
475
485
|
})
|
|
476
486
|
yield* drainPending()
|
|
487
|
+
if (chunk.fallback.restart) discardAttempt()
|
|
477
488
|
continue
|
|
478
489
|
}
|
|
479
490
|
|
|
@@ -947,10 +958,21 @@ export async function* streamProviderTurn(
|
|
|
947
958
|
|
|
948
959
|
// Not after a framing violation: the calls it would recover are the ones
|
|
949
960
|
// the stream was just refused for garbling.
|
|
961
|
+
//
|
|
962
|
+
// Nor when the stream was cut by a connection that may come back and no
|
|
963
|
+
// call came through whole: the retries are spent, the half-written call is
|
|
964
|
+
// not the model's mistake, and telling the model about it would spend a
|
|
965
|
+
// turn on a fault it cannot fix. The failure goes up and the turn pauses.
|
|
966
|
+
const cutByConnection =
|
|
967
|
+
streamError !== undefined &&
|
|
968
|
+
streamCause !== undefined &&
|
|
969
|
+
classifyProviderError(streamCause).retryable
|
|
950
970
|
const recoveredToolInputFromStreamError =
|
|
951
971
|
streamError !== undefined &&
|
|
952
972
|
!framingViolated &&
|
|
953
|
-
toolCalls.some(
|
|
973
|
+
toolCalls.some(
|
|
974
|
+
(tc) => tc.id && tc.function.name && (!cutByConnection || !tc.metadata?.inputError),
|
|
975
|
+
)
|
|
954
976
|
const effectiveFinishReason: ChatCompletionResponse['finishReason'] =
|
|
955
977
|
recoveredToolInputFromStreamError ? 'tool_calls' : finishReason
|
|
956
978
|
|
|
@@ -43,6 +43,7 @@ import { generateCheckpointId, generateTurnId } from '../../utils/id.js'
|
|
|
43
43
|
import { errorAttributes } from '../../utils/log/exception.js'
|
|
44
44
|
import type { Logger } from '../../utils/logger.js'
|
|
45
45
|
import { AUTO_APPROVE_POLICY_NAME, createSessionApprovalPolicy } from './approval-policy.js'
|
|
46
|
+
import { readCallsThatNeverStarted } from './calls-that-never-started.js'
|
|
46
47
|
import { type TurnContext, TurnContextFactory } from './context.js'
|
|
47
48
|
import { EventTranslator } from './events.js'
|
|
48
49
|
import type { QueryParams } from './index.js'
|
|
@@ -681,9 +682,17 @@ export async function prepareTurn(params: QueryParams): Promise<PreparedTurn> {
|
|
|
681
682
|
pinCompaction: true,
|
|
682
683
|
})),
|
|
683
684
|
])
|
|
684
|
-
|
|
685
|
+
let initialRepair = params.resumeFromCheckpoint
|
|
685
686
|
? { messages: projectedInitialMessages, report: undefined }
|
|
686
687
|
: repairToolMessageHistory(projectedInitialMessages)
|
|
688
|
+
if (initialRepair.report && initialRepair.report.syntheticToolResultsInserted > 0) {
|
|
689
|
+
// A call that was waiting for approval when the session ended never ran;
|
|
690
|
+
// the model is told so rather than that its outcome is unknown.
|
|
691
|
+
const neverRan = await readCallsThatNeverStarted(storage.log)
|
|
692
|
+
if (neverRan.size > 0) {
|
|
693
|
+
initialRepair = repairToolMessageHistory(projectedInitialMessages, { neverRan })
|
|
694
|
+
}
|
|
695
|
+
}
|
|
687
696
|
const initialMessages = initialRepair.messages
|
|
688
697
|
if (initialRepair.report && toolHistoryRepairChanged(initialRepair.report)) {
|
|
689
698
|
pendingHistoryRepairs.push({
|
|
@@ -46,7 +46,7 @@ const OWNED_WORK_DISPLAY_LIMIT = 16
|
|
|
46
46
|
|
|
47
47
|
/** What the model is told about owned work, wherever it is shown. */
|
|
48
48
|
const OWNED_WORK_GUIDANCE =
|
|
49
|
-
'Each task gives its id (taskId), its state, how its turn ended (turnStatus), why it stopped (stopReason), and whether its result has reached this conversation (resultDelivery: delivered-to-history, or not-delivered). Result delivery is not proof that you answered the original request. Answer the latest operator message and complete the requested synthesis of available results unless the operator cancelled or changed that request. Reuse delivered results when sufficient; do not relaunch completed work. If a delivered result is no longer visible, retrieve it by task ID. A terminal state with a non-end_turn stop reason is not successful task completion. Do not repeat a synthesis already given.'
|
|
49
|
+
'Each task gives its id (taskId), its state, how its turn ended (turnStatus), why it stopped (stopReason), and whether its result has reached this conversation (resultDelivery: delivered-to-history, or not-delivered). Result delivery is not proof that you answered the original request. Answer the latest operator message and complete the requested synthesis of available results unless the operator cancelled or changed that request. Reuse delivered results when sufficient; do not relaunch completed work. If a delivered result is no longer visible, retrieve it by task ID. A terminal state with a non-end_turn stop reason is not successful task completion. Results reach you and not the user, so before you end a reply check that the result of every finished task has been reported to the user, once. Do not repeat a synthesis already given.'
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* The ids of the tasks that earlier recorded entries name, read from the
|
|
@@ -809,8 +809,8 @@ export function formatCompletionNotification(handles: readonly TaskHandle[]): st
|
|
|
809
809
|
|
|
810
810
|
const preamble =
|
|
811
811
|
handles.length === 1
|
|
812
|
-
? 'A task you launched has finished. This is its result — you were not waiting on it, so it arrives here rather than as a tool result.'
|
|
813
|
-
: `${handles.length} tasks you launched have finished. These are their results — you were not waiting on them, so they arrive here rather than as tool results.`
|
|
812
|
+
? 'A task you launched has finished. This is its result — you were not waiting on it, so it arrives here rather than as a tool result. The user has not seen it; tell them what it found in your next message.'
|
|
813
|
+
: `${handles.length} tasks you launched have finished. These are their results — you were not waiting on them, so they arrive here rather than as tool results. The user has not seen them; tell them what each found in your next message.`
|
|
814
814
|
|
|
815
815
|
return `${preamble}\n\n${blocks.join('\n\n')}`
|
|
816
816
|
}
|
package/src/tools/admit-facts.ts
CHANGED
|
@@ -118,6 +118,7 @@ export function admitToolFacts(value: unknown): ToolFacts | undefined {
|
|
|
118
118
|
? { bytes: count(value.binary.bytes) }
|
|
119
119
|
: undefined,
|
|
120
120
|
background: flag(value.background),
|
|
121
|
+
continued: flag(value.continued),
|
|
121
122
|
})
|
|
122
123
|
case 'read': {
|
|
123
124
|
const path = text(value.path)
|
|
@@ -4,6 +4,7 @@ import { SANDBOX_KILL_GRACE_MS } from '../../constants/sandbox/index.js'
|
|
|
4
4
|
import { DANGEROUS_PATTERNS } from '../../constants/tools/index.js'
|
|
5
5
|
import { killTree } from '../../process/kill-tree.js'
|
|
6
6
|
import { genericLabel } from '../../registry/tool/presentation.js'
|
|
7
|
+
import { cancelCauseOf } from '../../types/session/cancel-cause.js'
|
|
7
8
|
import type { ToolFacts } from '../../types/tool/presentation.js'
|
|
8
9
|
import { subscribeToAbort } from '../../utils/abort.js'
|
|
9
10
|
import { readPositiveIntEnv } from '../../utils/env.js'
|
|
@@ -93,6 +94,28 @@ export function movedToBackgroundMessage(jobId: string, timeoutMs: number): stri
|
|
|
93
94
|
return `Command did not complete within its ${seconds}s timeout and was moved to the background as job ${jobId}; it was not stopped. You will be told when it ends. Its output so far is above.`
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
/**
|
|
98
|
+
* How long a foreground command runs before it is offered for hand-over to
|
|
99
|
+
* the background. A command that ends sooner is never interrupted by a stray
|
|
100
|
+
* key press, and a host that shows a hint shows it from this moment.
|
|
101
|
+
*/
|
|
102
|
+
export const FOREGROUND_HANDOFF_AFTER_MS = 2_000
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* What the model is told when the person moved a running foreground command to
|
|
106
|
+
* the background instead of stopping it. Exported so the test states the same
|
|
107
|
+
* words.
|
|
108
|
+
*/
|
|
109
|
+
export function handedOffMessage(jobId: string, reason: 'user' | 'message' | 'interrupt'): string {
|
|
110
|
+
const why =
|
|
111
|
+
reason === 'user'
|
|
112
|
+
? "at the person's request"
|
|
113
|
+
: reason === 'message'
|
|
114
|
+
? 'so a message from the person could reach you'
|
|
115
|
+
: 'because the person interrupted the turn'
|
|
116
|
+
return `Command was moved to the background as job ${jobId} ${why}; it was not stopped. You will be told when it ends. Its output so far is above.`
|
|
117
|
+
}
|
|
118
|
+
|
|
96
119
|
const inputSchema = z.object({
|
|
97
120
|
command: z
|
|
98
121
|
.string()
|
|
@@ -240,10 +263,34 @@ export function execHostShell(
|
|
|
240
263
|
child: ReturnType<typeof spawnHostShell>
|
|
241
264
|
initialOutput: readonly { stream: 'stdout' | 'stderr'; text: string }[]
|
|
242
265
|
}) => string | undefined
|
|
266
|
+
/**
|
|
267
|
+
* Takes the live process over for a hand-over the person asked for
|
|
268
|
+
* (a key press, a message that must reach the model, an interrupt),
|
|
269
|
+
* with the same contract as `onTimeout`. Absent, those hand-overs are
|
|
270
|
+
* not offered and an interrupt kills the command as it always did.
|
|
271
|
+
*/
|
|
272
|
+
adopt?: (running: {
|
|
273
|
+
child: ReturnType<typeof spawnHostShell>
|
|
274
|
+
initialOutput: readonly { stream: 'stdout' | 'stderr'; text: string }[]
|
|
275
|
+
}) => string | undefined
|
|
276
|
+
/**
|
|
277
|
+
* An interrupt the person caused (Esc, Ctrl+C) hands the command to
|
|
278
|
+
* `adopt` instead of killing it. Any other abort, a parent stopping a
|
|
279
|
+
* child or a budget stop, still kills it.
|
|
280
|
+
*/
|
|
281
|
+
adoptOnInterrupt?: boolean
|
|
282
|
+
/**
|
|
283
|
+
* Called once, right after the process is spawned, with the means to
|
|
284
|
+
* hand it over later. `background` returns whether the process was
|
|
285
|
+
* taken over; it settles the call with `backgroundedJobId` set.
|
|
286
|
+
*/
|
|
287
|
+
onRunning?: (control: { background(reason: 'user' | 'message'): boolean }) => void
|
|
243
288
|
},
|
|
244
289
|
): Promise<{
|
|
245
|
-
/** Set when
|
|
290
|
+
/** Set when the process was taken over; the command was not stopped. */
|
|
246
291
|
backgroundedJobId?: string
|
|
292
|
+
/** Why it was taken over. */
|
|
293
|
+
backgroundedBy?: 'timeout' | 'user' | 'message' | 'interrupt'
|
|
247
294
|
stdout: string
|
|
248
295
|
stderr: string
|
|
249
296
|
/** Bytes captured on each stream, before decoding; absent from a runner that does not count them. */
|
|
@@ -401,44 +448,64 @@ export function execHostShell(
|
|
|
401
448
|
)
|
|
402
449
|
}
|
|
403
450
|
child.once('close', onClose)
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
closed = true
|
|
421
|
-
disposeAbort?.()
|
|
422
|
-
child.stdout?.off('data', onStdout)
|
|
423
|
-
child.stderr?.off('data', onStderr)
|
|
424
|
-
child.off('close', onClose)
|
|
425
|
-
child.off('error', onError)
|
|
426
|
-
child.off('spawn', onSpawn)
|
|
427
|
-
resolve({
|
|
428
|
-
backgroundedJobId: jobId,
|
|
429
|
-
stdout: decodeCapture('stdout'),
|
|
430
|
-
stderr: decodeCapture('stderr'),
|
|
431
|
-
})
|
|
432
|
-
return
|
|
433
|
-
}
|
|
451
|
+
// Hands the live process to the host's job registry, once. `onTimeout`
|
|
452
|
+
// answers for the deadline; every other reason goes to `adopt`.
|
|
453
|
+
const takeOver = (reason: 'timeout' | 'user' | 'message' | 'interrupt'): boolean => {
|
|
454
|
+
const adopter =
|
|
455
|
+
reason === 'timeout' ? options.onTimeout : (options.adopt ?? options.onTimeout)
|
|
456
|
+
if (!adopter || closed || cause !== undefined) return false
|
|
457
|
+
let jobId: string | undefined
|
|
458
|
+
try {
|
|
459
|
+
jobId = adopter({
|
|
460
|
+
child,
|
|
461
|
+
initialOutput: (['stdout', 'stderr'] as const)
|
|
462
|
+
.map((stream) => ({ stream, text: decodeCapture(stream) }))
|
|
463
|
+
.filter((part) => part.text !== ''),
|
|
464
|
+
})
|
|
465
|
+
} catch {
|
|
466
|
+
// A host that cannot take the process over leaves the process where it is.
|
|
434
467
|
}
|
|
468
|
+
if (jobId === undefined) return false
|
|
469
|
+
// The registry owns the process from here: stop listening to it,
|
|
470
|
+
// so a later Stop or its exit is not this call's business.
|
|
471
|
+
closed = true
|
|
472
|
+
disposeAbort?.()
|
|
473
|
+
clearTimeout(deadline)
|
|
474
|
+
child.stdout?.off('data', onStdout)
|
|
475
|
+
child.stderr?.off('data', onStderr)
|
|
476
|
+
child.off('close', onClose)
|
|
477
|
+
child.off('error', onError)
|
|
478
|
+
child.off('spawn', onSpawn)
|
|
479
|
+
resolve({
|
|
480
|
+
backgroundedJobId: jobId,
|
|
481
|
+
backgroundedBy: reason,
|
|
482
|
+
stdout: decodeCapture('stdout'),
|
|
483
|
+
stderr: decodeCapture('stderr'),
|
|
484
|
+
})
|
|
485
|
+
return true
|
|
486
|
+
}
|
|
487
|
+
const deadline = setTimeout(() => {
|
|
488
|
+
if (takeOver('timeout')) return
|
|
435
489
|
cancel('timeout')
|
|
436
490
|
}, options.timeout)
|
|
437
491
|
deadline.unref?.()
|
|
438
492
|
if (options.signal) {
|
|
439
|
-
|
|
440
|
-
|
|
493
|
+
const signal = options.signal
|
|
494
|
+
const onAbort = () => {
|
|
495
|
+
// Esc or Ctrl+C keeps what the command was doing; any other stop
|
|
496
|
+
// (a parent cancelling a child, a budget) still ends it.
|
|
497
|
+
if (
|
|
498
|
+
options.adoptOnInterrupt === true &&
|
|
499
|
+
cancelCauseOf(signal.reason) === 'user' &&
|
|
500
|
+
takeOver('interrupt')
|
|
501
|
+
)
|
|
502
|
+
return
|
|
503
|
+
cancel('caller')
|
|
504
|
+
}
|
|
505
|
+
disposeAbort = subscribeToAbort(signal, onAbort)
|
|
506
|
+
if (signal.aborted) onAbort()
|
|
441
507
|
}
|
|
508
|
+
options.onRunning?.({ background: (reason) => takeOver(reason) })
|
|
442
509
|
})
|
|
443
510
|
}
|
|
444
511
|
|
|
@@ -714,51 +781,76 @@ const BashToolBase = defineTool({
|
|
|
714
781
|
// registry, a sandboxed turn, a one-off sandbox escape, a plain wait)
|
|
715
782
|
// is there anything to hand the process to, so those keep the kill.
|
|
716
783
|
const adopt = context.backgroundJobs?.adopt?.bind(context.backgroundJobs)
|
|
717
|
-
const
|
|
718
|
-
adopt !== undefined &&
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
784
|
+
const canHandOff =
|
|
785
|
+
adopt !== undefined && context.sandbox === undefined && !backgroundTasksDisabled()
|
|
786
|
+
const canMoveOnTimeout = canHandOff && !startsWithSleep(input.command)
|
|
787
|
+
const adoptRunning = (running: {
|
|
788
|
+
child: Parameters<NonNullable<typeof adopt>>[0]['child']
|
|
789
|
+
initialOutput: Parameters<NonNullable<typeof adopt>>[0]['initialOutput']
|
|
790
|
+
}) =>
|
|
791
|
+
adopt?.({
|
|
792
|
+
command: input.command,
|
|
793
|
+
child: running.child,
|
|
794
|
+
workingDirectory: context.workingDirectory,
|
|
795
|
+
initialOutput: running.initialOutput,
|
|
796
|
+
}).id
|
|
797
|
+
// Offered for hand-over once it has run long enough to be worth moving.
|
|
798
|
+
let offerTimer: ReturnType<typeof setTimeout> | undefined
|
|
799
|
+
let withdrawOffer: (() => void) | undefined
|
|
800
|
+
const offer = context.backgroundJobs?.registerForeground?.bind(context.backgroundJobs)
|
|
722
801
|
|
|
723
802
|
try {
|
|
724
|
-
const { stdout, stderr, stdoutBytes, stderrBytes, backgroundedJobId } =
|
|
725
|
-
input.command,
|
|
726
|
-
{
|
|
803
|
+
const { stdout, stderr, stdoutBytes, stderrBytes, backgroundedJobId, backgroundedBy } =
|
|
804
|
+
await execHostShell(input.command, {
|
|
727
805
|
cwd: context.workingDirectory,
|
|
728
806
|
timeout: input.timeout,
|
|
729
807
|
env: { ...inherited.env, ...context.env },
|
|
730
808
|
maxBuffer: DEFAULT_BASH_MAX_BUFFER_BYTES,
|
|
731
809
|
signal: context.abortSignal,
|
|
732
810
|
onOutput,
|
|
733
|
-
...(
|
|
811
|
+
...(canMoveOnTimeout ? { onTimeout: adoptRunning } : {}),
|
|
812
|
+
...(canHandOff
|
|
734
813
|
? {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
814
|
+
adopt: adoptRunning,
|
|
815
|
+
adoptOnInterrupt: true,
|
|
816
|
+
...(offer
|
|
817
|
+
? {
|
|
818
|
+
onRunning: (control: {
|
|
819
|
+
background(reason: 'user' | 'message'): boolean
|
|
820
|
+
}) => {
|
|
821
|
+
offerTimer = setTimeout(() => {
|
|
822
|
+
withdrawOffer = offer({
|
|
823
|
+
command: input.command,
|
|
824
|
+
background: (reason) => control.background(reason),
|
|
825
|
+
})
|
|
826
|
+
}, FOREGROUND_HANDOFF_AFTER_MS)
|
|
827
|
+
offerTimer.unref?.()
|
|
828
|
+
},
|
|
829
|
+
}
|
|
830
|
+
: {}),
|
|
742
831
|
}
|
|
743
832
|
: {}),
|
|
744
|
-
}
|
|
745
|
-
)
|
|
833
|
+
})
|
|
746
834
|
if (backgroundedJobId !== undefined) {
|
|
835
|
+
const timedOut = backgroundedBy === 'timeout'
|
|
747
836
|
return {
|
|
748
837
|
success: true,
|
|
749
838
|
output: [
|
|
750
839
|
formatShellOutput(stdout, stderr),
|
|
751
|
-
|
|
840
|
+
timedOut || backgroundedBy === undefined
|
|
841
|
+
? movedToBackgroundMessage(backgroundedJobId, input.timeout)
|
|
842
|
+
: handedOffMessage(backgroundedJobId, backgroundedBy),
|
|
752
843
|
]
|
|
753
844
|
.filter(Boolean)
|
|
754
845
|
.join('\n\n'),
|
|
755
846
|
data: {
|
|
756
847
|
jobId: backgroundedJobId,
|
|
757
848
|
background: true,
|
|
758
|
-
timedOut: true,
|
|
849
|
+
...(timedOut || backgroundedBy === undefined ? { timedOut: true } : {}),
|
|
759
850
|
movedToBackground: true,
|
|
851
|
+
...(backgroundedBy ? { backgroundedBy } : {}),
|
|
760
852
|
},
|
|
761
|
-
facts: { kind: 'command', command: input.command, background: true },
|
|
853
|
+
facts: { kind: 'command', command: input.command, background: true, continued: true },
|
|
762
854
|
}
|
|
763
855
|
}
|
|
764
856
|
return {
|
|
@@ -856,6 +948,9 @@ const BashToolBase = defineTool({
|
|
|
856
948
|
...(failure.stderrBytes !== undefined ? { stderrBytes: failure.stderrBytes } : {}),
|
|
857
949
|
}),
|
|
858
950
|
}
|
|
951
|
+
} finally {
|
|
952
|
+
if (offerTimer !== undefined) clearTimeout(offerTimer)
|
|
953
|
+
withdrawOffer?.()
|
|
859
954
|
}
|
|
860
955
|
},
|
|
861
956
|
})
|