@namzu/sdk 53.1.0 → 53.2.1
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 +19 -0
- 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/salience/bm25.d.ts +1 -1
- package/dist/compaction/salience/bm25.d.ts.map +1 -1
- package/dist/compaction/salience/bm25.js +1 -2
- package/dist/compaction/salience/bm25.js.map +1 -1
- package/dist/compaction/salience/score.d.ts.map +1 -1
- package/dist/compaction/salience/score.js +1 -1
- package/dist/compaction/salience/score.js.map +1 -1
- package/dist/compaction/salience/tokenize.d.ts.map +1 -1
- package/dist/compaction/salience/tokenize.js +16 -2
- package/dist/compaction/salience/tokenize.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/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/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/salience/bm25.ts +4 -2
- package/src/compaction/salience/score.ts +3 -1
- package/src/compaction/salience/tokenize.ts +16 -2
- 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/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/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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/types/tool/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/types/tool/index.ts"],"names":[],"mappings":"AAyzCA,cAAc,aAAa,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@namzu/sdk",
|
|
3
|
-
"version": "53.1
|
|
3
|
+
"version": "53.2.1",
|
|
4
4
|
"description": "Open-source AI kernel for TypeScript: runs an agent the way an operating system runs a process, with an identity and a budget, scheduled tool calls and checkpointed turns.",
|
|
5
5
|
"license": "FSL-1.1-MIT",
|
|
6
6
|
"engines": {
|
package/src/compaction/index.ts
CHANGED
|
@@ -71,7 +71,12 @@ export type { ToolResultEditConfig, ToolResultEditOutcome } from './tool-result-
|
|
|
71
71
|
// can offer, rather than something that only happens when a threshold trips.
|
|
72
72
|
export { compactNow, compactRegion } from './manual.js'
|
|
73
73
|
export type { CompactionResult, CompactNowInput, CompactRegionInput } from './manual.js'
|
|
74
|
-
export {
|
|
74
|
+
export {
|
|
75
|
+
COMPACTION_HEADER,
|
|
76
|
+
COMPACTION_RECOVERY_NOTE,
|
|
77
|
+
buildCompactionMessage,
|
|
78
|
+
isCompactionMessage,
|
|
79
|
+
} from './summary.js'
|
|
75
80
|
|
|
76
81
|
// The salience-scored working set: every message scored, the least salient
|
|
77
82
|
// evicted first. See `docs/sdk/salience-working-set.md`.
|
package/src/compaction/manual.ts
CHANGED
|
@@ -63,6 +63,11 @@ export interface CompactNowInput extends CompactionVerificationOptions {
|
|
|
63
63
|
* a no-op. The callback owns persistence and must not mutate the messages.
|
|
64
64
|
*/
|
|
65
65
|
readonly onShed?: (messages: readonly Message[]) => void | Promise<void>
|
|
66
|
+
/**
|
|
67
|
+
* A paragraph added to the end of the summary, for a host that can point the
|
|
68
|
+
* model at the originals ({@link COMPACTION_RECOVERY_NOTE}). Absent adds none.
|
|
69
|
+
*/
|
|
70
|
+
readonly recoveryNote?: string
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
async function publishManualCompaction(
|
|
@@ -100,12 +105,13 @@ function splice(
|
|
|
100
105
|
summaryBody: string,
|
|
101
106
|
retainedOlder: readonly Message[],
|
|
102
107
|
recentMessages: readonly Message[],
|
|
108
|
+
recoveryNote?: string,
|
|
103
109
|
): { messages: Message[]; summary: Message } {
|
|
104
110
|
// A host-triggered pass has no turn-scoped WorkingStateManager to carry the
|
|
105
111
|
// summary into a later query. Pin this summary itself: until a future turn
|
|
106
112
|
// has rebuilt equivalent state, this message is the only surviving record
|
|
107
113
|
// of what the pass removed.
|
|
108
|
-
const summary = { ...buildCompactionMessage(summaryBody), retain: true }
|
|
114
|
+
const summary = { ...buildCompactionMessage(summaryBody, recoveryNote), retain: true }
|
|
109
115
|
return { messages: [...preservedSystem, summary, ...retainedOlder, ...recentMessages], summary }
|
|
110
116
|
}
|
|
111
117
|
|
|
@@ -205,7 +211,13 @@ export async function compactNow(input: CompactNowInput): Promise<CompactionResu
|
|
|
205
211
|
},
|
|
206
212
|
)
|
|
207
213
|
|
|
208
|
-
const { messages, summary } = splice(
|
|
214
|
+
const { messages, summary } = splice(
|
|
215
|
+
preservedSystem,
|
|
216
|
+
body,
|
|
217
|
+
retainedOlder,
|
|
218
|
+
plan.recentMessages,
|
|
219
|
+
input.recoveryNote,
|
|
220
|
+
)
|
|
209
221
|
return publishManualCompaction(input, {
|
|
210
222
|
messages,
|
|
211
223
|
shed: input.messages.length - messages.length,
|
|
@@ -294,7 +306,7 @@ export async function compactRegion(input: CompactRegionInput): Promise<Compacti
|
|
|
294
306
|
|
|
295
307
|
// Same cross-run ownership as compactNow: a selected region has been
|
|
296
308
|
// replaced outside a turn, so its only surviving account is pinned.
|
|
297
|
-
const summary = { ...buildCompactionMessage(body), retain: true }
|
|
309
|
+
const summary = { ...buildCompactionMessage(body, input.recoveryNote), retain: true }
|
|
298
310
|
const out = [...messages.slice(0, start), summary, ...retainedSelected, ...messages.slice(end)]
|
|
299
311
|
|
|
300
312
|
// Checked after the splice, not only before it. The edges being
|
|
@@ -27,8 +27,10 @@ export interface Bm25Index {
|
|
|
27
27
|
const K1 = 1.2
|
|
28
28
|
const B = 0.75
|
|
29
29
|
|
|
30
|
-
export function indexDocument(
|
|
31
|
-
|
|
30
|
+
export function indexDocument(
|
|
31
|
+
text: string,
|
|
32
|
+
tokens: readonly string[] = tokenize(text),
|
|
33
|
+
): Bm25Document {
|
|
32
34
|
return { tf: termFrequencies(tokens), length: tokens.length }
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -132,7 +132,9 @@ export function scoreMessages(
|
|
|
132
132
|
const texts = messages.map(messageText)
|
|
133
133
|
const richContent = messages.map(hasRichContent)
|
|
134
134
|
const tokenLists = texts.map(tokenize)
|
|
135
|
-
const documents: Bm25Document[] = texts.map(
|
|
135
|
+
const documents: Bm25Document[] = texts.map((text, i) =>
|
|
136
|
+
indexDocument(text, tokenLists[i] as string[]),
|
|
137
|
+
)
|
|
136
138
|
const index = buildIndex(documents)
|
|
137
139
|
const goalTokens = tokenize(options.goal)
|
|
138
140
|
|
|
@@ -46,10 +46,24 @@ const STOP_WORDS: ReadonlySet<string> = new Set([
|
|
|
46
46
|
|
|
47
47
|
const WORD = /[\p{L}\p{N}_$][\p{L}\p{N}_$.\-/\\:]*/gu
|
|
48
48
|
|
|
49
|
+
const SEPARATORS: ReadonlySet<string> = new Set(['.', '-', '/', '\\', ':'])
|
|
50
|
+
|
|
51
|
+
// A linear scan from each end: a `[sep]+$` regex is retried from every start of a
|
|
52
|
+
// long separator run inside a token and fails at the end each time, quadratically.
|
|
53
|
+
function trimSeparators(token: string): string {
|
|
54
|
+
let start = 0
|
|
55
|
+
let end = token.length
|
|
56
|
+
while (start < end && SEPARATORS.has(token[start] as string)) start++
|
|
57
|
+
while (end > start && SEPARATORS.has(token[end - 1] as string)) end--
|
|
58
|
+
return start === 0 && end === token.length ? token : token.slice(start, end)
|
|
59
|
+
}
|
|
60
|
+
|
|
49
61
|
function splitIdentifier(raw: string): string[] {
|
|
50
62
|
const parts = raw
|
|
51
63
|
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
52
|
-
|
|
64
|
+
// Lookaround keeps this linear: a capturing `([A-Z]+)([A-Z][a-z])` backtracks
|
|
65
|
+
// quadratically on a long run of capitals that never reaches a lower-case letter.
|
|
66
|
+
.replace(/(?<=[A-Z])(?=[A-Z][a-z])/g, ' ')
|
|
53
67
|
.split(/[_\s]+/)
|
|
54
68
|
.filter((p) => p.length > 0)
|
|
55
69
|
return parts.length > 1 ? parts : []
|
|
@@ -59,7 +73,7 @@ function splitIdentifier(raw: string): string[] {
|
|
|
59
73
|
export function tokenize(text: string): string[] {
|
|
60
74
|
const out: string[] = []
|
|
61
75
|
for (const match of text.matchAll(WORD)) {
|
|
62
|
-
const raw = match[0]
|
|
76
|
+
const raw = trimSeparators(match[0])
|
|
63
77
|
if (raw.length === 0) continue
|
|
64
78
|
const whole = raw.toLowerCase()
|
|
65
79
|
if (!STOP_WORDS.has(whole)) out.push(whole)
|
|
@@ -25,10 +25,24 @@ export function isCompactionMessage(content: string | null | undefined): boolean
|
|
|
25
25
|
return typeof content === 'string' && content.startsWith(COMPACTION_HEADER)
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/**
|
|
29
|
-
|
|
28
|
+
/**
|
|
29
|
+
* The line that tells the model where exact earlier text can still be had. A
|
|
30
|
+
* host that mounts `search_conversation` and `read_conversation` adds it to
|
|
31
|
+
* every summary; a host without them adds nothing, because a pointer to a tool
|
|
32
|
+
* that is not there is worse than none.
|
|
33
|
+
*/
|
|
34
|
+
export const COMPACTION_RECOVERY_NOTE =
|
|
35
|
+
'Earlier output was cut from this summary. To get exact earlier text, call search_conversation, then read_conversation.'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The summary, as the system message that replaces what it summarises.
|
|
39
|
+
* `recoveryNote` is appended as its own paragraph.
|
|
40
|
+
*/
|
|
41
|
+
export function buildCompactionMessage(body: string, recoveryNote?: string): Message {
|
|
30
42
|
return {
|
|
31
|
-
...createSystemMessage(
|
|
43
|
+
...createSystemMessage(
|
|
44
|
+
`${COMPACTION_HEADER}\n\n${body}${recoveryNote ? `\n\n${recoveryNote}` : ''}`,
|
|
45
|
+
),
|
|
32
46
|
source: { type: 'compaction-summary' },
|
|
33
47
|
}
|
|
34
48
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { CHARS_PER_TOKEN } from '../constants/limits.js'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
describeRetainedOutputRecovery,
|
|
4
|
+
retainedOutputPointer,
|
|
5
|
+
} from '../runtime/query/tool-output-budget.js'
|
|
3
6
|
import { toolResultToText } from '../types/message/content.js'
|
|
4
7
|
import type { Message, ToolMessage, ToolResultBlock } from '../types/message/index.js'
|
|
5
8
|
import { findRetainedIndices } from './retention.js'
|
|
@@ -68,8 +71,8 @@ const KEEP_TAIL_CHARS = 200
|
|
|
68
71
|
* back to it. Replacing the whole content destroyed the cheapest recovery
|
|
69
72
|
* route precisely for the largest results — and then told the model to
|
|
70
73
|
* call the tool again, which is advice to re-run something that returned
|
|
71
|
-
* megabytes. Keeping
|
|
72
|
-
* more than the
|
|
74
|
+
* megabytes. Keeping the line and its recovery sentence preserves a route
|
|
75
|
+
* worth far more than the two lines cost.
|
|
73
76
|
*
|
|
74
77
|
* A HEAD AND TAIL, because a result is not uniformly valuable: what a
|
|
75
78
|
* model needs from a long output is usually near one end, and keeping a
|
|
@@ -86,12 +89,12 @@ function clearedPlaceholder(
|
|
|
86
89
|
`${CLEARED_PREFIX} ${toolName} returned ${size.toLocaleString('en-US')} characters, cleared to reclaim context.`,
|
|
87
90
|
]
|
|
88
91
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
)
|
|
92
|
+
// The pointer is the spill line with the sentence that came with it, so the
|
|
93
|
+
// route is stated once, where the output was cut, and never contradicted here.
|
|
94
|
+
const pointer = retainedOutputPointer(text)
|
|
95
|
+
if (pointer) {
|
|
96
|
+
// A pointer written before the sentence existed gets the neutral one.
|
|
97
|
+
parts.push(pointer.includes('\n') ? pointer : `${pointer}\n${describeRetainedOutputRecovery()}`)
|
|
95
98
|
} else {
|
|
96
99
|
parts.push(
|
|
97
100
|
'Full result unavailable here. Recover from a saved artifact or a read-only observation; do not repeat a state-changing action to recover its output.',
|
|
@@ -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,
|
|
@@ -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
|