@namzu/sdk 53.0.0 → 53.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -1
- package/dist/compaction/index.d.ts +1 -1
- package/dist/compaction/index.d.ts.map +1 -1
- package/dist/compaction/index.js +1 -1
- package/dist/compaction/index.js.map +1 -1
- package/dist/compaction/manual.d.ts +5 -0
- package/dist/compaction/manual.d.ts.map +1 -1
- package/dist/compaction/manual.js +4 -4
- package/dist/compaction/manual.js.map +1 -1
- package/dist/compaction/summary.d.ts +12 -2
- package/dist/compaction/summary.d.ts.map +1 -1
- package/dist/compaction/summary.js +13 -3
- package/dist/compaction/summary.js.map +1 -1
- package/dist/compaction/tool-result-editing.d.ts.map +1 -1
- package/dist/compaction/tool-result-editing.js +9 -6
- package/dist/compaction/tool-result-editing.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts +15 -0
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +169 -1
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/public-runtime.d.ts +2 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +2 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/runtime/query/checkpoint.d.ts.map +1 -1
- package/dist/runtime/query/checkpoint.js +3 -1
- package/dist/runtime/query/checkpoint.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +2 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +21 -1
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +6 -2
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/tool-output-budget.d.ts +30 -2
- package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
- package/dist/runtime/query/tool-output-budget.js +43 -10
- package/dist/runtime/query/tool-output-budget.js.map +1 -1
- package/dist/scheduler/local.d.ts +2 -1
- package/dist/scheduler/local.d.ts.map +1 -1
- package/dist/scheduler/local.js +8 -0
- package/dist/scheduler/local.js.map +1 -1
- package/dist/store/evidence/disk.d.ts +3 -2
- package/dist/store/evidence/disk.d.ts.map +1 -1
- package/dist/store/evidence/disk.js +23 -3
- package/dist/store/evidence/disk.js.map +1 -1
- package/dist/store/evidence/source-text.d.ts +20 -0
- package/dist/store/evidence/source-text.d.ts.map +1 -1
- package/dist/store/evidence/source-text.js +29 -2
- package/dist/store/evidence/source-text.js.map +1 -1
- package/dist/store/session-log/core.d.ts.map +1 -1
- package/dist/store/session-log/core.js +28 -0
- package/dist/store/session-log/core.js.map +1 -1
- package/dist/store/session-log/spilled-input.d.ts +34 -0
- package/dist/store/session-log/spilled-input.d.ts.map +1 -0
- package/dist/store/session-log/spilled-input.js +68 -0
- package/dist/store/session-log/spilled-input.js.map +1 -0
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +7 -2
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/retained-output.d.ts +12 -0
- package/dist/tools/retained-output.d.ts.map +1 -0
- package/dist/tools/retained-output.js +39 -0
- package/dist/tools/retained-output.js.map +1 -0
- package/dist/types/agent/manager.d.ts +48 -0
- package/dist/types/agent/manager.d.ts.map +1 -1
- package/dist/types/agent/scheduler.d.ts +20 -0
- package/dist/types/agent/scheduler.d.ts.map +1 -1
- package/dist/types/agent/task.d.ts +2 -0
- package/dist/types/agent/task.d.ts.map +1 -1
- package/dist/types/session/records.d.ts +25 -0
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +16 -0
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/tool/index.d.ts +8 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/package.json +1 -1
- package/src/compaction/index.ts +6 -1
- package/src/compaction/manual.ts +15 -3
- package/src/compaction/summary.ts +17 -3
- package/src/compaction/tool-result-editing.ts +12 -9
- package/src/manager/agent/lifecycle.ts +181 -1
- package/src/public-runtime.ts +2 -1
- package/src/runtime/query/checkpoint.ts +6 -1
- package/src/runtime/query/executor.ts +25 -1
- package/src/runtime/query/iteration/phases/compaction.ts +13 -2
- package/src/runtime/query/tool-output-budget.ts +65 -11
- package/src/scheduler/local.ts +14 -1
- package/src/store/evidence/disk.ts +28 -3
- package/src/store/evidence/source-text.ts +38 -2
- package/src/store/session-log/core.ts +28 -0
- package/src/store/session-log/spilled-input.ts +90 -0
- package/src/tools/builtins/read-file.ts +7 -2
- package/src/tools/retained-output.ts +38 -0
- package/src/types/agent/manager.ts +54 -0
- package/src/types/agent/scheduler.ts +21 -0
- package/src/types/agent/task.ts +2 -0
- package/src/types/session/records.ts +16 -0
- package/src/types/tool/index.ts +8 -0
|
@@ -23,6 +23,11 @@ import type {
|
|
|
23
23
|
AgentLifecycleEvent,
|
|
24
24
|
AgentLifecycleListener,
|
|
25
25
|
} from '../../types/agent/lifecycle-event.js'
|
|
26
|
+
import type {
|
|
27
|
+
RestoreChildOptions,
|
|
28
|
+
RestoreChildRefusal,
|
|
29
|
+
RestoreChildResult,
|
|
30
|
+
} from '../../types/agent/manager.js'
|
|
26
31
|
import type {
|
|
27
32
|
AgentManagerConfig,
|
|
28
33
|
AgentTask,
|
|
@@ -53,7 +58,12 @@ import { type Logger, resolveLogger } from '../../utils/logger.js'
|
|
|
53
58
|
import { requireOpenProject } from '../project/lifecycle.js'
|
|
54
59
|
import { type TopicManagerDependency, resolveTopicManager } from '../topic/dependency.js'
|
|
55
60
|
import type { TopicManager } from '../topic/lifecycle.js'
|
|
56
|
-
import {
|
|
61
|
+
import {
|
|
62
|
+
childSessionEnded,
|
|
63
|
+
readChildSessionMeta,
|
|
64
|
+
registerChildSessionLog,
|
|
65
|
+
writeChildSessionMeta,
|
|
66
|
+
} from './child-session.js'
|
|
57
67
|
|
|
58
68
|
/**
|
|
59
69
|
* Dependencies threaded into {@link AgentManager}. Phase 6 promoted the
|
|
@@ -155,6 +165,7 @@ interface ChildSpawnRecord {
|
|
|
155
165
|
readonly toolCallId: string
|
|
156
166
|
readonly agentType: string
|
|
157
167
|
readonly description: string
|
|
168
|
+
readonly continuation?: NonNullable<ChildSessionMeta['continuation']>
|
|
158
169
|
}
|
|
159
170
|
/** Removes the child's log from the process-local lookup. */
|
|
160
171
|
releaseLog?: () => void
|
|
@@ -1236,6 +1247,7 @@ export class AgentManager {
|
|
|
1236
1247
|
description: placement.description,
|
|
1237
1248
|
status: 'running',
|
|
1238
1249
|
createdAt: placement.createdAt,
|
|
1250
|
+
...(placement.continuation ? { continuation: placement.continuation } : {}),
|
|
1239
1251
|
}
|
|
1240
1252
|
}
|
|
1241
1253
|
|
|
@@ -1296,6 +1308,14 @@ export class AgentManager {
|
|
|
1296
1308
|
toolCallId: spawnToolCallId(taskId),
|
|
1297
1309
|
agentType: options.agentId,
|
|
1298
1310
|
description: describeSpawn(options),
|
|
1311
|
+
...(options.continuationProfile
|
|
1312
|
+
? {
|
|
1313
|
+
continuation: {
|
|
1314
|
+
workspace: options.workspace?.mode === 'isolated' ? 'isolated' : 'shared',
|
|
1315
|
+
profile: { ...options.continuationProfile },
|
|
1316
|
+
},
|
|
1317
|
+
}
|
|
1318
|
+
: {}),
|
|
1299
1319
|
}
|
|
1300
1320
|
await writeChildSessionMeta(spawnRecord.placement.metaPath, this.childMeta(spawnRecord))
|
|
1301
1321
|
return DiskSessionLog.at(paths, {
|
|
@@ -1796,6 +1816,166 @@ export class AgentManager {
|
|
|
1796
1816
|
})
|
|
1797
1817
|
}
|
|
1798
1818
|
|
|
1819
|
+
/**
|
|
1820
|
+
* Make a child saved by an earlier process continuable again.
|
|
1821
|
+
*
|
|
1822
|
+
* Reads the child's meta document and log from the project layout under the
|
|
1823
|
+
* invoking parent, checks they describe a settled, shared-workspace child of
|
|
1824
|
+
* this very parent session, then re-creates the in-memory session and edge
|
|
1825
|
+
* and the retained conversation that {@link provisionContinuation} requires.
|
|
1826
|
+
* It runs nothing; the caller continues the child with `sendMessage` and
|
|
1827
|
+
* `resumeSessionId`, so the invoking turn's authority, limits and review
|
|
1828
|
+
* channel apply exactly as for a child that finished in this process.
|
|
1829
|
+
*/
|
|
1830
|
+
async restoreChildConversation(
|
|
1831
|
+
options: RestoreChildOptions,
|
|
1832
|
+
context: AgentTaskContext,
|
|
1833
|
+
): Promise<RestoreChildResult> {
|
|
1834
|
+
const refuse = (reason: RestoreChildRefusal): RestoreChildResult => ({
|
|
1835
|
+
status: 'refused',
|
|
1836
|
+
reason,
|
|
1837
|
+
})
|
|
1838
|
+
const id = options.childSessionId
|
|
1839
|
+
const retained = this.childConversations.get(id)
|
|
1840
|
+
if (retained?.activeSubSessionId) return refuse('running')
|
|
1841
|
+
const paths =
|
|
1842
|
+
context.childStorage?.kind === 'disk' ? context.childStorage.paths : this.deps.paths
|
|
1843
|
+
if (!paths) return refuse('no-layout')
|
|
1844
|
+
const store = this.deps.sessionStore
|
|
1845
|
+
if (store.supportsInvocationSummaries !== true || store.supportsOwnerVersionCas !== true)
|
|
1846
|
+
return refuse('not-resumable')
|
|
1847
|
+
const ancestry = await store.getAncestry(context.sessionId, context.tenantId)
|
|
1848
|
+
const parentLocator: SessionLocator = {
|
|
1849
|
+
sessionId: context.sessionId,
|
|
1850
|
+
ancestors: ancestry.slice(0, -1),
|
|
1851
|
+
}
|
|
1852
|
+
let meta: ChildSessionMeta | null
|
|
1853
|
+
try {
|
|
1854
|
+
meta = await readChildSessionMeta(paths.subagentMeta(parentLocator, id))
|
|
1855
|
+
} catch {
|
|
1856
|
+
return refuse('not-saved')
|
|
1857
|
+
}
|
|
1858
|
+
if (!meta) return refuse('not-saved')
|
|
1859
|
+
if (meta.sessionId !== id || meta.parentSessionId !== context.sessionId)
|
|
1860
|
+
return refuse('foreign')
|
|
1861
|
+
const continuation = meta.continuation
|
|
1862
|
+
if (!continuation || continuation.workspace !== 'shared') return refuse('not-resumable')
|
|
1863
|
+
const agentId = options.agentFor(continuation.profile)
|
|
1864
|
+
if (agentId === undefined || !this.registry.has(agentId)) return refuse('not-resumable')
|
|
1865
|
+
const log = retained?.log ?? DiskSessionLog.at(paths, { sessionId: id, ancestors: ancestry })
|
|
1866
|
+
// The child's own log wins over its meta document: a child that never
|
|
1867
|
+
// recorded the end of a turn died mid-turn, and what it did is unknown.
|
|
1868
|
+
const ended = await childSessionEnded(log)
|
|
1869
|
+
if (!ended) return refuse('unsettled')
|
|
1870
|
+
if (ended.status === 'cancelled' || meta.status === 'cancelled') return refuse('stopped')
|
|
1871
|
+
// A log written under another project or topic cannot be continued here:
|
|
1872
|
+
// the child's turn would refuse to open it.
|
|
1873
|
+
const started = await log.head()
|
|
1874
|
+
if (started === null) return refuse('unsettled')
|
|
1875
|
+
for await (const { record } of log.read({ mode: 'tolerant' })) {
|
|
1876
|
+
if (record.type !== 'session_started') continue
|
|
1877
|
+
if (record.projectId !== context.projectId || record.topicId !== context.topicId)
|
|
1878
|
+
return refuse('foreign')
|
|
1879
|
+
break
|
|
1880
|
+
}
|
|
1881
|
+
const placement = {
|
|
1882
|
+
paths,
|
|
1883
|
+
metaPath: paths.subagentMeta(parentLocator, id),
|
|
1884
|
+
createdAt: meta.createdAt,
|
|
1885
|
+
toolCallId: meta.toolCallId,
|
|
1886
|
+
agentType: meta.agentType,
|
|
1887
|
+
description: meta.description,
|
|
1888
|
+
continuation,
|
|
1889
|
+
}
|
|
1890
|
+
const restored: RestoreChildResult = {
|
|
1891
|
+
status: 'restored',
|
|
1892
|
+
childSessionId: id,
|
|
1893
|
+
agentId,
|
|
1894
|
+
profile: continuation.profile,
|
|
1895
|
+
description: meta.description,
|
|
1896
|
+
}
|
|
1897
|
+
if (retained) return restored
|
|
1898
|
+
if (this.childConversations.size >= RETAINED_CHILD_CONVERSATION_CAP) {
|
|
1899
|
+
const oldest = [...this.childConversations].find(([, value]) => !value.activeSubSessionId)
|
|
1900
|
+
if (!oldest) return refuse('full')
|
|
1901
|
+
this.childConversations.delete(oldest[0])
|
|
1902
|
+
}
|
|
1903
|
+
const childActor: ActorRef = {
|
|
1904
|
+
kind: 'agent',
|
|
1905
|
+
agentId,
|
|
1906
|
+
tenantId: context.tenantId,
|
|
1907
|
+
parentActor: context.parentActor,
|
|
1908
|
+
}
|
|
1909
|
+
let child = await store.getSession(id, context.tenantId)
|
|
1910
|
+
let createdChild = false
|
|
1911
|
+
let edge: Awaited<ReturnType<typeof store.createSubSession>> | undefined
|
|
1912
|
+
let createdEdge = false
|
|
1913
|
+
try {
|
|
1914
|
+
if (child) {
|
|
1915
|
+
// A store that outlived the process still holds the child and its edge.
|
|
1916
|
+
if (
|
|
1917
|
+
child.status === 'active' ||
|
|
1918
|
+
child.status === 'archived' ||
|
|
1919
|
+
child.status === 'locked' ||
|
|
1920
|
+
child.projectId !== context.projectId ||
|
|
1921
|
+
child.topicId !== context.topicId ||
|
|
1922
|
+
child.currentActor?.kind !== 'agent' ||
|
|
1923
|
+
child.currentActor.agentId !== agentId
|
|
1924
|
+
)
|
|
1925
|
+
return refuse('running')
|
|
1926
|
+
} else {
|
|
1927
|
+
child = await store.createSession(
|
|
1928
|
+
{ id, topicId: context.topicId, projectId: context.projectId, currentActor: childActor },
|
|
1929
|
+
context.tenantId,
|
|
1930
|
+
)
|
|
1931
|
+
createdChild = true
|
|
1932
|
+
}
|
|
1933
|
+
edge = (await store.getChildren(context.sessionId, context.tenantId)).find(
|
|
1934
|
+
(candidate) => candidate.childSessionId === id,
|
|
1935
|
+
)
|
|
1936
|
+
if (!edge) {
|
|
1937
|
+
edge = await store.createSubSession(
|
|
1938
|
+
{
|
|
1939
|
+
parentSessionId: context.sessionId,
|
|
1940
|
+
childSessionId: id,
|
|
1941
|
+
kind: 'agent_spawn',
|
|
1942
|
+
spawnedBy: context.parentActor,
|
|
1943
|
+
failureMode: 'delegate',
|
|
1944
|
+
completionMode: 'summary_ref',
|
|
1945
|
+
},
|
|
1946
|
+
context.tenantId,
|
|
1947
|
+
)
|
|
1948
|
+
createdEdge = true
|
|
1949
|
+
}
|
|
1950
|
+
} catch (error) {
|
|
1951
|
+
if (edge && createdEdge)
|
|
1952
|
+
await store.deleteSubSession(edge.id, context.tenantId).catch(() => undefined)
|
|
1953
|
+
if (child && createdChild)
|
|
1954
|
+
await store.deleteSession(id, context.tenantId).catch(() => undefined)
|
|
1955
|
+
throw error
|
|
1956
|
+
}
|
|
1957
|
+
this.childConversations.set(id, {
|
|
1958
|
+
definition: this.registry.getOrThrow(agentId),
|
|
1959
|
+
origin: {
|
|
1960
|
+
subSessionId: edge.id,
|
|
1961
|
+
childSessionId: id,
|
|
1962
|
+
tenantId: context.tenantId,
|
|
1963
|
+
parentSessionId: context.sessionId,
|
|
1964
|
+
parentTurnId: meta.parentTurnId,
|
|
1965
|
+
rootSessionId: meta.rootSessionId,
|
|
1966
|
+
ancestry,
|
|
1967
|
+
childDepth: meta.depth,
|
|
1968
|
+
childOwnerVersion: child.ownerVersion,
|
|
1969
|
+
placement,
|
|
1970
|
+
},
|
|
1971
|
+
topicId: context.topicId,
|
|
1972
|
+
projectId: context.projectId,
|
|
1973
|
+
cwd: resolve(options.workingDirectory),
|
|
1974
|
+
log,
|
|
1975
|
+
})
|
|
1976
|
+
return restored
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1799
1979
|
private releaseChildConversation(record: ChildSpawnRecord): void {
|
|
1800
1980
|
const conversation = this.childConversations.get(record.childSessionId)
|
|
1801
1981
|
if (conversation?.activeSubSessionId !== record.subSessionId) return
|
package/src/public-runtime.ts
CHANGED
|
@@ -1275,7 +1275,7 @@ export { compactNow, compactRegion } from './compaction/manual.js'
|
|
|
1275
1275
|
// A host persisting manual compaction has to recognise the system message on
|
|
1276
1276
|
// resume. Export the identity predicate rather than making every host copy the
|
|
1277
1277
|
// marker string and eventually disagree with the writer.
|
|
1278
|
-
export { isCompactionMessage } from './compaction/summary.js'
|
|
1278
|
+
export { COMPACTION_RECOVERY_NOTE, isCompactionMessage } from './compaction/summary.js'
|
|
1279
1279
|
export {
|
|
1280
1280
|
DEFAULT_SALIENCE_CONFIG,
|
|
1281
1281
|
DEFAULT_SALIENCE_WEIGHTS,
|
|
@@ -1639,6 +1639,7 @@ export {
|
|
|
1639
1639
|
createSessionEvidenceSource,
|
|
1640
1640
|
createSessionTextEvidenceSource,
|
|
1641
1641
|
} from './store/evidence/disk.js'
|
|
1642
|
+
export { SessionEvidenceOffsetError } from './store/evidence/source-text.js'
|
|
1642
1643
|
export {
|
|
1643
1644
|
classifyEvidenceSource,
|
|
1644
1645
|
EVIDENCE_RECORD_GUIDANCE,
|
|
@@ -2,6 +2,7 @@ import type { WorkingStateSnapshot } from '../../compaction/wire.js'
|
|
|
2
2
|
import { type TurnRecorder, readFoldedHistory } from '../../manager/session/turn-recorder.js'
|
|
3
3
|
import type { CheckpointScope, SessionCheckpointStore } from '../../store/checkpoint/index.js'
|
|
4
4
|
import type { SessionLog } from '../../store/session-log/index.js'
|
|
5
|
+
import { restoreSpilledRequestInputs } from '../../store/session-log/spilled-input.js'
|
|
5
6
|
import type { SerializedSpanContext } from '../../telemetry/attributes.js'
|
|
6
7
|
import { NamzuError } from '../../types/errors/index.js'
|
|
7
8
|
import type {
|
|
@@ -148,7 +149,11 @@ export async function readParks(
|
|
|
148
149
|
decisionId: r.decisionId,
|
|
149
150
|
turnId: r.turnId as TurnId,
|
|
150
151
|
checkpointId: r.checkpointId,
|
|
151
|
-
|
|
152
|
+
// A call input too large for a line was spilled; put it back.
|
|
153
|
+
request: (await restoreSpilledRequestInputs(
|
|
154
|
+
structuredClone(r.request) as unknown as HITLDecisionRequest & { toolCalls?: unknown },
|
|
155
|
+
(ref) => log.readSpill(ref),
|
|
156
|
+
)) as HITLDecisionRequest,
|
|
152
157
|
parkedAt: Date.parse(r.ts),
|
|
153
158
|
...(r.deadlineAt !== undefined ? { deadlineAt: Date.parse(r.deadlineAt) } : {}),
|
|
154
159
|
})
|
|
@@ -17,6 +17,7 @@ import { sandboxShellSpawn, withoutBashStartup } from '../../tools/command-shell
|
|
|
17
17
|
import { isAlwaysDestructive } from '../../tools/defineTool.js'
|
|
18
18
|
import { createFileReadTracker } from '../../tools/file-read-tracker.js'
|
|
19
19
|
import { pathOutsideRoots, toolRoots } from '../../tools/paths.js'
|
|
20
|
+
import { resolveRetainedOutput } from '../../tools/retained-output.js'
|
|
20
21
|
import type { ToolManager } from '../../toolsets/manager.js'
|
|
21
22
|
import type { ToolSourceRef } from '../../toolsets/types.js'
|
|
22
23
|
import type { ToolResultGuardrailSpec } from '../../types/guardrail/index.js'
|
|
@@ -81,6 +82,7 @@ import { ToolCallBudget, assertMaxToolCalls } from './tool-call-budget.js'
|
|
|
81
82
|
import {
|
|
82
83
|
DEFAULT_MAX_TOOL_OUTPUT_CHARS,
|
|
83
84
|
DEFAULT_MAX_TOOL_OUTPUT_CHARS_PER_MESSAGE,
|
|
85
|
+
type RetainedOutputRoutes,
|
|
84
86
|
type ToolOutputBudgetResult,
|
|
85
87
|
applyToolOutputBudget,
|
|
86
88
|
describeDroppedContent,
|
|
@@ -635,6 +637,17 @@ export function deniedToolOutput(toolName: string, reason: string): string {
|
|
|
635
637
|
}
|
|
636
638
|
|
|
637
639
|
export class ToolExecutor {
|
|
640
|
+
/** The recovery routes this turn actually has, so a pointer never names a missing tool. */
|
|
641
|
+
private recoveryRoutes(): RetainedOutputRoutes {
|
|
642
|
+
const tools = this.config.tools
|
|
643
|
+
return {
|
|
644
|
+
read: tools.get('read') !== undefined,
|
|
645
|
+
conversation:
|
|
646
|
+
tools.get('search_conversation') !== undefined &&
|
|
647
|
+
tools.get('read_conversation') !== undefined,
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
638
651
|
private outputDirectory(): string | undefined {
|
|
639
652
|
return typeof this.config.toolOutputDir === 'function'
|
|
640
653
|
? this.config.toolOutputDir()
|
|
@@ -1079,7 +1092,11 @@ export class ToolExecutor {
|
|
|
1079
1092
|
) {
|
|
1080
1093
|
const value = input[tool.pathArgument]
|
|
1081
1094
|
if (typeof value === 'string') {
|
|
1082
|
-
|
|
1095
|
+
// This session's own retained output is not "outside": it is where the
|
|
1096
|
+
// result that was cut told the model to look.
|
|
1097
|
+
const retained =
|
|
1098
|
+
tool.name === 'read' && (await resolveRetainedOutput(this.outputDirectory(), value))
|
|
1099
|
+
const outside = retained ? undefined : await pathOutsideRoots(toolRoots(this.config), value)
|
|
1083
1100
|
if (outside !== undefined) outsidePaths = [outside]
|
|
1084
1101
|
}
|
|
1085
1102
|
}
|
|
@@ -1526,6 +1543,7 @@ export class ToolExecutor {
|
|
|
1526
1543
|
{
|
|
1527
1544
|
maxChars,
|
|
1528
1545
|
spillDir: this.outputDirectory(),
|
|
1546
|
+
recoveryRoutes: this.recoveryRoutes(),
|
|
1529
1547
|
onError: (message) =>
|
|
1530
1548
|
this.log.warn('Failed to retain a tool output replaced for the message budget', {
|
|
1531
1549
|
[NAMZU.TURN_ID]: this.config.turnId,
|
|
@@ -1806,6 +1824,7 @@ export class ToolExecutor {
|
|
|
1806
1824
|
maxChars: this.config.maxToolOutputChars ?? DEFAULT_MAX_TOOL_OUTPUT_CHARS,
|
|
1807
1825
|
retainedPreviewChars: this.config.retainedToolPreviewChars,
|
|
1808
1826
|
spillDir: this.outputDirectory(),
|
|
1827
|
+
recoveryRoutes: this.recoveryRoutes(),
|
|
1809
1828
|
onError: (message) =>
|
|
1810
1829
|
this.log.warn('Failed to spill oversized nested tool output', {
|
|
1811
1830
|
[NAMZU.TURN_ID]: this.config.turnId,
|
|
@@ -1901,6 +1920,9 @@ export class ToolExecutor {
|
|
|
1901
1920
|
...(this.config.additionalDirectories?.length
|
|
1902
1921
|
? { additionalDirectories: this.config.additionalDirectories }
|
|
1903
1922
|
: {}),
|
|
1923
|
+
...(this.outputDirectory()
|
|
1924
|
+
? { retainedOutputDirectory: this.outputDirectory() as string }
|
|
1925
|
+
: {}),
|
|
1904
1926
|
abortSignal: this.config.abortSignal,
|
|
1905
1927
|
env: this.config.env,
|
|
1906
1928
|
log: (level, message) => this.log[level](message),
|
|
@@ -2287,6 +2309,7 @@ export class ToolExecutor {
|
|
|
2287
2309
|
maxChars: maxToolOutputChars,
|
|
2288
2310
|
retainedPreviewChars: this.config.retainedToolPreviewChars,
|
|
2289
2311
|
spillDir: this.outputDirectory(),
|
|
2312
|
+
recoveryRoutes: this.recoveryRoutes(),
|
|
2290
2313
|
onError: (message) =>
|
|
2291
2314
|
this.log.warn('Failed to retain original tool output', {
|
|
2292
2315
|
[NAMZU.TURN_ID]: this.config.turnId,
|
|
@@ -3177,6 +3200,7 @@ export class ToolExecutor {
|
|
|
3177
3200
|
spillDir: this.outputDirectory()
|
|
3178
3201
|
? join(this.outputDirectory() as string, 'content')
|
|
3179
3202
|
: undefined,
|
|
3203
|
+
recoveryRoutes: this.recoveryRoutes(),
|
|
3180
3204
|
onError: (message) =>
|
|
3181
3205
|
this.log.warn('Failed to spill oversized model tool content', {
|
|
3182
3206
|
[NAMZU.TURN_ID]: this.config.turnId,
|
|
@@ -13,7 +13,11 @@ import type { ContextReducer, ContextReduction } from '../../../../compaction/re
|
|
|
13
13
|
import { createSlidingWindowReducer } from '../../../../compaction/reducer.js'
|
|
14
14
|
import { findRetainedIndices } from '../../../../compaction/retention.js'
|
|
15
15
|
import { serializeState } from '../../../../compaction/serializer.js'
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
COMPACTION_RECOVERY_NOTE,
|
|
18
|
+
buildCompactionMessage,
|
|
19
|
+
isCompactionMessage,
|
|
20
|
+
} from '../../../../compaction/summary.js'
|
|
17
21
|
import { estimateMessagesTokens } from '../../../../compaction/token-estimate.js'
|
|
18
22
|
import { buildVerifiedSummaryWithBoundedProvider } from '../../../../compaction/verifier.js'
|
|
19
23
|
import type { CompactionConfig } from '../../../../config/runtime.js'
|
|
@@ -999,7 +1003,14 @@ async function runCompactionCheckInner(
|
|
|
999
1003
|
compactedContent = serializeState(manager.getState())
|
|
1000
1004
|
}
|
|
1001
1005
|
|
|
1002
|
-
|
|
1006
|
+
// The pointer is only worth a line when both tools it names are mounted.
|
|
1007
|
+
const mounted = new Set(ctx.tools?.listNames?.() ?? [])
|
|
1008
|
+
const compactionMessage = buildCompactionMessage(
|
|
1009
|
+
compactedContent,
|
|
1010
|
+
mounted.has('search_conversation') && mounted.has('read_conversation')
|
|
1011
|
+
? COMPACTION_RECOVERY_NOTE
|
|
1012
|
+
: undefined,
|
|
1013
|
+
)
|
|
1003
1014
|
|
|
1004
1015
|
// Drop a replaceable PRIOR `[COMPACTED CONTEXT]` summary from this turn's
|
|
1005
1016
|
// leading floor — `serializeState` is cumulative, so the new summary
|
|
@@ -42,6 +42,58 @@ export const MESSAGE_BUDGET_PREVIEW_CHARS = 2_000
|
|
|
42
42
|
*/
|
|
43
43
|
export const SPILL_MARKER = 'The full output was written to:'
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* The recovery routes a host has mounted for a retained output. A pointer
|
|
47
|
+
* names only a tool that exists, so the same sentence is true wherever it is
|
|
48
|
+
* read: in the result, in the cleared placeholder that keeps it, and in a
|
|
49
|
+
* summary that quotes it.
|
|
50
|
+
*/
|
|
51
|
+
export interface RetainedOutputRoutes {
|
|
52
|
+
/** The `read` tool is mounted and may open this session's own retained files. */
|
|
53
|
+
readonly read?: boolean
|
|
54
|
+
/** `search_conversation` and `read_conversation` are both mounted. */
|
|
55
|
+
readonly conversation?: boolean
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Opening of the sentence that follows the spill line; how a pointer is recognised. */
|
|
59
|
+
export const RETAINED_OUTPUT_NOTE = 'This path identifies retained output, not the original input.'
|
|
60
|
+
|
|
61
|
+
const REPLAY_WARNING =
|
|
62
|
+
'A fresh observation of the original input cannot recover its earlier contents. If recovery is unavailable, report the missing detail; do not replay the original action.'
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The one sentence that says how to get at a retained output. Without routes
|
|
66
|
+
* (an embedder that told the budget nothing) it keeps the neutral wording.
|
|
67
|
+
*/
|
|
68
|
+
export function describeRetainedOutputRecovery(routes?: RetainedOutputRoutes): string {
|
|
69
|
+
const read = routes?.read === true
|
|
70
|
+
const conversation = routes?.conversation === true
|
|
71
|
+
const how = [
|
|
72
|
+
read ? 'To read more of it, open this file with the read tool (offset and limit).' : '',
|
|
73
|
+
conversation
|
|
74
|
+
? read
|
|
75
|
+
? 'For anything else from earlier in this conversation, call search_conversation with a phrase from it, then read_conversation with the seq and part it returns.'
|
|
76
|
+
: 'To read more of it, call search_conversation with a phrase from it, then read_conversation with the seq and part it returns. The path is not a workspace file, so read and grep cannot open it.'
|
|
77
|
+
: '',
|
|
78
|
+
].filter(Boolean)
|
|
79
|
+
if (how.length === 0)
|
|
80
|
+
return `${RETAINED_OUTPUT_NOTE} Use the host-authorized retained-output recovery tools when available; the path does not grant filesystem access. ${REPLAY_WARNING}`
|
|
81
|
+
return [RETAINED_OUTPUT_NOTE, ...how, REPLAY_WARNING].join(' ')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The spill line and the recovery sentence under it, when `text` carries them.
|
|
86
|
+
* Whatever shortens a result keeps this pair, so the route survives.
|
|
87
|
+
*/
|
|
88
|
+
export function retainedOutputPointer(text: string): string | undefined {
|
|
89
|
+
// A tool may end its lines with \r\n or a bare \r; the pointer is one line either way.
|
|
90
|
+
const lines = text.split(/\r\n|\r|\n/)
|
|
91
|
+
const at = lines.findIndex((line) => line.startsWith(SPILL_MARKER))
|
|
92
|
+
if (at < 0) return undefined
|
|
93
|
+
const next = lines[at + 1]
|
|
94
|
+
return next?.startsWith(RETAINED_OUTPUT_NOTE) ? `${lines[at]}\n${next}` : (lines[at] as string)
|
|
95
|
+
}
|
|
96
|
+
|
|
45
97
|
/**
|
|
46
98
|
* Share of the budget spent on the head. The rest goes to the tail.
|
|
47
99
|
*
|
|
@@ -91,7 +143,7 @@ function boundedPreview(
|
|
|
91
143
|
'',
|
|
92
144
|
].join('\n')
|
|
93
145
|
const compactMiddle = '\n[... omitted ...]\n'
|
|
94
|
-
const spillLine = recovery.split(
|
|
146
|
+
const spillLine = recovery.split(/\r\n|\r|\n/).find((line) => line.startsWith(SPILL_MARKER))
|
|
95
147
|
const compactRecovery = spillLine ? `${compactMiddle}${spillLine}\n` : compactMiddle
|
|
96
148
|
const middle =
|
|
97
149
|
detailedMiddle.length < maxChars
|
|
@@ -182,6 +234,8 @@ export interface ApplyToolOutputBudgetOptions {
|
|
|
182
234
|
* middle-elided instead — degraded, but never unbounded.
|
|
183
235
|
*/
|
|
184
236
|
readonly spillDir?: string | undefined
|
|
237
|
+
/** Which recovery routes the host has mounted; see {@link describeRetainedOutputRecovery}. */
|
|
238
|
+
readonly recoveryRoutes?: RetainedOutputRoutes
|
|
185
239
|
readonly onError?: (message: string) => void
|
|
186
240
|
}
|
|
187
241
|
|
|
@@ -189,8 +243,9 @@ export interface ApplyToolOutputBudgetOptions {
|
|
|
189
243
|
* Bound a tool result to the model-visible budget.
|
|
190
244
|
*
|
|
191
245
|
* Retention keeps the original while bounding its model-visible preview.
|
|
192
|
-
* The
|
|
193
|
-
*
|
|
246
|
+
* The recovery sentence names only the routes the host says are mounted
|
|
247
|
+
* ({@link RetainedOutputRoutes}); a spill path alone is not proof that a
|
|
248
|
+
* workspace tool can open it. Middle-elision is the
|
|
194
249
|
* fallback for a turn with no directory to write to.
|
|
195
250
|
*
|
|
196
251
|
* The preview keeps head AND tail because the two ends carry different
|
|
@@ -224,10 +279,9 @@ export function applyToolOutputBudget(opts: ApplyToolOutputBudgetOptions): ToolO
|
|
|
224
279
|
|
|
225
280
|
const spillPath = retained?.path
|
|
226
281
|
const recovery = spillPath
|
|
227
|
-
? [
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
].join('\n')
|
|
282
|
+
? [`${SPILL_MARKER} ${spillPath}`, describeRetainedOutputRecovery(opts.recoveryRoutes)].join(
|
|
283
|
+
'\n',
|
|
284
|
+
)
|
|
231
285
|
: 'The full output was not retained. Use a saved artifact or a read-only observation; do not repeat a state-changing action to recover its output.'
|
|
232
286
|
const requestedPreview = opts.retainedPreviewChars
|
|
233
287
|
// Separate the spill threshold from the cost of carrying its preview on every
|
|
@@ -358,11 +412,10 @@ export interface MessageBudgetReplacement {
|
|
|
358
412
|
export interface FitResultsToMessageBudgetOptions {
|
|
359
413
|
readonly maxChars: number
|
|
360
414
|
readonly spillDir?: string | undefined
|
|
415
|
+
readonly recoveryRoutes?: RetainedOutputRoutes
|
|
361
416
|
readonly onError?: (message: string) => void
|
|
362
417
|
}
|
|
363
418
|
|
|
364
|
-
const SPILL_LINE = new RegExp(`^${SPILL_MARKER} .+$`, 'm')
|
|
365
|
-
|
|
366
419
|
/**
|
|
367
420
|
* Hold one assistant message's combined tool results under a cap.
|
|
368
421
|
*
|
|
@@ -409,7 +462,7 @@ function previewForMessageBudget(
|
|
|
409
462
|
item: MessageBudgetItem,
|
|
410
463
|
opts: FitResultsToMessageBudgetOptions,
|
|
411
464
|
): MessageBudgetReplacement {
|
|
412
|
-
const spilledAt =
|
|
465
|
+
const spilledAt = retainedOutputPointer(item.output)
|
|
413
466
|
if (spilledAt !== undefined) {
|
|
414
467
|
const output = [
|
|
415
468
|
safeHead(item.output, MESSAGE_BUDGET_PREVIEW_CHARS / 2),
|
|
@@ -420,7 +473,7 @@ function previewForMessageBudget(
|
|
|
420
473
|
return {
|
|
421
474
|
output,
|
|
422
475
|
originalLength: item.output.length,
|
|
423
|
-
spillPath: spilledAt.slice(SPILL_MARKER.length + 1),
|
|
476
|
+
spillPath: (spilledAt.split('\n')[0] as string).slice(SPILL_MARKER.length + 1),
|
|
424
477
|
}
|
|
425
478
|
}
|
|
426
479
|
const budgeted = applyToolOutputBudget({
|
|
@@ -430,6 +483,7 @@ function previewForMessageBudget(
|
|
|
430
483
|
output: item.output,
|
|
431
484
|
maxChars: MESSAGE_BUDGET_PREVIEW_CHARS,
|
|
432
485
|
spillDir: opts.spillDir,
|
|
486
|
+
...(opts.recoveryRoutes ? { recoveryRoutes: opts.recoveryRoutes } : {}),
|
|
433
487
|
...(opts.onError ? { onError: opts.onError } : {}),
|
|
434
488
|
})
|
|
435
489
|
return {
|
package/src/scheduler/local.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { GENAI } from '../constants/telemetry/index.js'
|
|
2
2
|
import { taskFailed } from '../tools/coordinator/outcome.js'
|
|
3
3
|
import type { AgentInput } from '../types/agent/base.js'
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
AgentManagerContract,
|
|
6
|
+
RestoreChildOptions,
|
|
7
|
+
RestoreChildResult,
|
|
8
|
+
} from '../types/agent/manager.js'
|
|
5
9
|
import type {
|
|
6
10
|
CreateTaskOptions,
|
|
7
11
|
SiblingFailurePolicy,
|
|
@@ -113,6 +117,9 @@ export class LocalTaskScheduler implements TaskScheduler {
|
|
|
113
117
|
{
|
|
114
118
|
agentId: options.agentId,
|
|
115
119
|
...(options.resumeSessionId ? { resumeSessionId: options.resumeSessionId } : {}),
|
|
120
|
+
...(options.continuationProfile
|
|
121
|
+
? { continuationProfile: options.continuationProfile }
|
|
122
|
+
: {}),
|
|
116
123
|
beforeStart: options.beforeStart,
|
|
117
124
|
...(options.workspace ? { workspace: options.workspace } : {}),
|
|
118
125
|
...(options.planId ? { planId: options.planId } : {}),
|
|
@@ -363,6 +370,12 @@ export class LocalTaskScheduler implements TaskScheduler {
|
|
|
363
370
|
await this.agentManager.continueTask(taskId, message)
|
|
364
371
|
}
|
|
365
372
|
|
|
373
|
+
async restoreChild(options: RestoreChildOptions): Promise<RestoreChildResult> {
|
|
374
|
+
if (!this.agentManager.restoreChildConversation)
|
|
375
|
+
return { status: 'refused', reason: 'no-layout' }
|
|
376
|
+
return this.agentManager.restoreChildConversation(options, this.taskContext)
|
|
377
|
+
}
|
|
378
|
+
|
|
366
379
|
cancelTask(taskId: TaskId, cause?: CancelCause): void {
|
|
367
380
|
this.agentManager.cancel(taskId, cause)
|
|
368
381
|
}
|
|
@@ -110,8 +110,9 @@ export interface EvidenceAnchor {
|
|
|
110
110
|
* session log, or one turn of it. Closed by default (no active turn in scope);
|
|
111
111
|
* snapshot mode also reads a session whose turn is running, without assuming
|
|
112
112
|
* its writer is dead. The host owns authorization and the log's directory.
|
|
113
|
-
*
|
|
114
|
-
*
|
|
113
|
+
* An address names a record by offset, length and digest and survives appends
|
|
114
|
+
* to the log; a replaced or rewritten log is refused and record and chunk
|
|
115
|
+
* digests detect changed bytes. This is not a hostile filesystem sandbox.
|
|
115
116
|
*/
|
|
116
117
|
export function createSessionTextEvidenceSource(
|
|
117
118
|
options: SessionEvidenceSourceOptions,
|
|
@@ -261,9 +262,18 @@ function createSource(
|
|
|
261
262
|
if (!state.closed && consistency === 'closed') {
|
|
262
263
|
throw new Error('Evidence scope has an active turn; it is not closed.')
|
|
263
264
|
}
|
|
265
|
+
// The anchored prefix is verified here and again after the action. It is not
|
|
266
|
+
// part of the seal: the anchor moves with every record the writer appends, so
|
|
267
|
+
// binding it would void an address the moment the search call that returned
|
|
268
|
+
// it is itself recorded.
|
|
264
269
|
const anchorKey = anchor ? await verifyAnchor(handle, anchor) : undefined
|
|
270
|
+
// A session log only grows. An address names one record by offset, length and
|
|
271
|
+
// digest, and a read verifies all three against the bytes, so what must
|
|
272
|
+
// bind a seal is the file's identity, never its size or times: those change
|
|
273
|
+
// with every append and would void every address and cursor handed out a
|
|
274
|
+
// moment earlier, including one a search just returned.
|
|
265
275
|
const sourceKey = digest(
|
|
266
|
-
`session-v1:${consistency}:${mode}:${scopeKey}:${
|
|
276
|
+
`session-v1:${consistency}:${mode}:${scopeKey}:${fileIdentity(before)}`,
|
|
267
277
|
)
|
|
268
278
|
const seal = evidenceSeal(state.first, sourceKey)
|
|
269
279
|
const value = await action(handle, size, seal, sourceKey, budget, !state.closed)
|
|
@@ -552,6 +562,21 @@ function createSource(
|
|
|
552
562
|
})
|
|
553
563
|
}
|
|
554
564
|
|
|
565
|
+
/**
|
|
566
|
+
* What names the log file across appends: device and inode, plus the creation
|
|
567
|
+
* time where the filesystem reports no inode (zero), so a replaced file still
|
|
568
|
+
* changes the identity there.
|
|
569
|
+
*/
|
|
570
|
+
function fileIdentity(stats: {
|
|
571
|
+
dev: number | bigint
|
|
572
|
+
ino: number | bigint
|
|
573
|
+
birthtimeMs: number
|
|
574
|
+
}): string {
|
|
575
|
+
return Number(stats.ino) === 0
|
|
576
|
+
? `${stats.dev}:0:${stats.birthtimeMs}`
|
|
577
|
+
: `${stats.dev}:${stats.ino}`
|
|
578
|
+
}
|
|
579
|
+
|
|
555
580
|
/** The anchor record must still hold the bytes the writer vouched for. */
|
|
556
581
|
async function verifyAnchor(handle: FileHandle, anchor: EvidenceAnchor): Promise<string> {
|
|
557
582
|
const bytes = await readBytes(handle, anchor.offset, anchor.length, {
|
|
@@ -235,6 +235,28 @@ async function sourceText(
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* @experimental A read named a byte offset the retained text cannot start at.
|
|
240
|
+
* `inside_character` means the offset falls inside a multi-byte character (the
|
|
241
|
+
* character starts up to three bytes earlier); `past_end` means it lies beyond
|
|
242
|
+
* the text. Every other read failure (integrity, ownership, a changed source)
|
|
243
|
+
* is a plain `Error`, so a caller can recover from a bad position without
|
|
244
|
+
* treating a refusal as one.
|
|
245
|
+
*/
|
|
246
|
+
export class SessionEvidenceOffsetError extends Error {
|
|
247
|
+
constructor(
|
|
248
|
+
readonly code: 'inside_character' | 'past_end',
|
|
249
|
+
message: string,
|
|
250
|
+
/** Bytes of the retained text. */
|
|
251
|
+
readonly totalBytes: number,
|
|
252
|
+
/** Bytes this failed read was charged. */
|
|
253
|
+
readonly scannedBytes: number,
|
|
254
|
+
) {
|
|
255
|
+
super(message)
|
|
256
|
+
this.name = 'SessionEvidenceOffsetError'
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
238
260
|
export async function readTextPage(
|
|
239
261
|
source: TextSource,
|
|
240
262
|
scope: SessionEvidenceScope,
|
|
@@ -242,13 +264,27 @@ export async function readTextPage(
|
|
|
242
264
|
budget: EvidenceBudget,
|
|
243
265
|
): Promise<SessionTextEvidenceReadResult> {
|
|
244
266
|
const entry = source.entry
|
|
245
|
-
if (offset > source.bytes)
|
|
267
|
+
if (offset > source.bytes)
|
|
268
|
+
throw new SessionEvidenceOffsetError(
|
|
269
|
+
'past_end',
|
|
270
|
+
'Offset exceeds retained text.',
|
|
271
|
+
source.bytes,
|
|
272
|
+
budget.bytes,
|
|
273
|
+
)
|
|
246
274
|
const chunk = entry.spill
|
|
247
275
|
? Math.min(Math.floor(offset / EVIDENCE_CHUNK_BYTES), source.chunks - 1)
|
|
248
276
|
: 0
|
|
249
277
|
const window = await source.window(chunk)
|
|
250
|
-
|
|
278
|
+
const splits = () =>
|
|
279
|
+
new SessionEvidenceOffsetError(
|
|
280
|
+
'inside_character',
|
|
281
|
+
'Offset splits a UTF-8 character.',
|
|
282
|
+
source.bytes,
|
|
283
|
+
budget.bytes,
|
|
284
|
+
)
|
|
285
|
+
if (offset < window.offset) throw splits()
|
|
251
286
|
const bytes = window.bytes.subarray(offset - window.offset)
|
|
287
|
+
if (bytes.length && ((bytes[0] ?? 0) & 0xc0) === 0x80) throw splits()
|
|
252
288
|
let text = decode(utf8Page(bytes, 24_000))
|
|
253
289
|
if (text.length > 6000) {
|
|
254
290
|
let end = 6000
|