@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.
Files changed (100) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/dist/compaction/index.d.ts +1 -1
  3. package/dist/compaction/index.d.ts.map +1 -1
  4. package/dist/compaction/index.js +1 -1
  5. package/dist/compaction/index.js.map +1 -1
  6. package/dist/compaction/manual.d.ts +5 -0
  7. package/dist/compaction/manual.d.ts.map +1 -1
  8. package/dist/compaction/manual.js +4 -4
  9. package/dist/compaction/manual.js.map +1 -1
  10. package/dist/compaction/summary.d.ts +12 -2
  11. package/dist/compaction/summary.d.ts.map +1 -1
  12. package/dist/compaction/summary.js +13 -3
  13. package/dist/compaction/summary.js.map +1 -1
  14. package/dist/compaction/tool-result-editing.d.ts.map +1 -1
  15. package/dist/compaction/tool-result-editing.js +9 -6
  16. package/dist/compaction/tool-result-editing.js.map +1 -1
  17. package/dist/manager/agent/lifecycle.d.ts +15 -0
  18. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  19. package/dist/manager/agent/lifecycle.js +169 -1
  20. package/dist/manager/agent/lifecycle.js.map +1 -1
  21. package/dist/public-runtime.d.ts +2 -1
  22. package/dist/public-runtime.d.ts.map +1 -1
  23. package/dist/public-runtime.js +2 -1
  24. package/dist/public-runtime.js.map +1 -1
  25. package/dist/runtime/query/checkpoint.d.ts.map +1 -1
  26. package/dist/runtime/query/checkpoint.js +3 -1
  27. package/dist/runtime/query/checkpoint.js.map +1 -1
  28. package/dist/runtime/query/executor.d.ts +2 -0
  29. package/dist/runtime/query/executor.d.ts.map +1 -1
  30. package/dist/runtime/query/executor.js +21 -1
  31. package/dist/runtime/query/executor.js.map +1 -1
  32. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  33. package/dist/runtime/query/iteration/phases/compaction.js +6 -2
  34. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  35. package/dist/runtime/query/tool-output-budget.d.ts +30 -2
  36. package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
  37. package/dist/runtime/query/tool-output-budget.js +43 -10
  38. package/dist/runtime/query/tool-output-budget.js.map +1 -1
  39. package/dist/scheduler/local.d.ts +2 -1
  40. package/dist/scheduler/local.d.ts.map +1 -1
  41. package/dist/scheduler/local.js +8 -0
  42. package/dist/scheduler/local.js.map +1 -1
  43. package/dist/store/evidence/disk.d.ts +3 -2
  44. package/dist/store/evidence/disk.d.ts.map +1 -1
  45. package/dist/store/evidence/disk.js +23 -3
  46. package/dist/store/evidence/disk.js.map +1 -1
  47. package/dist/store/evidence/source-text.d.ts +20 -0
  48. package/dist/store/evidence/source-text.d.ts.map +1 -1
  49. package/dist/store/evidence/source-text.js +29 -2
  50. package/dist/store/evidence/source-text.js.map +1 -1
  51. package/dist/store/session-log/core.d.ts.map +1 -1
  52. package/dist/store/session-log/core.js +28 -0
  53. package/dist/store/session-log/core.js.map +1 -1
  54. package/dist/store/session-log/spilled-input.d.ts +34 -0
  55. package/dist/store/session-log/spilled-input.d.ts.map +1 -0
  56. package/dist/store/session-log/spilled-input.js +68 -0
  57. package/dist/store/session-log/spilled-input.js.map +1 -0
  58. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  59. package/dist/tools/builtins/read-file.js +7 -2
  60. package/dist/tools/builtins/read-file.js.map +1 -1
  61. package/dist/tools/retained-output.d.ts +12 -0
  62. package/dist/tools/retained-output.d.ts.map +1 -0
  63. package/dist/tools/retained-output.js +39 -0
  64. package/dist/tools/retained-output.js.map +1 -0
  65. package/dist/types/agent/manager.d.ts +48 -0
  66. package/dist/types/agent/manager.d.ts.map +1 -1
  67. package/dist/types/agent/scheduler.d.ts +20 -0
  68. package/dist/types/agent/scheduler.d.ts.map +1 -1
  69. package/dist/types/agent/task.d.ts +2 -0
  70. package/dist/types/agent/task.d.ts.map +1 -1
  71. package/dist/types/session/records.d.ts +25 -0
  72. package/dist/types/session/records.d.ts.map +1 -1
  73. package/dist/types/session/records.js +16 -0
  74. package/dist/types/session/records.js.map +1 -1
  75. package/dist/types/tool/index.d.ts +8 -0
  76. package/dist/types/tool/index.d.ts.map +1 -1
  77. package/dist/types/tool/index.js.map +1 -1
  78. package/package.json +1 -1
  79. package/src/compaction/index.ts +6 -1
  80. package/src/compaction/manual.ts +15 -3
  81. package/src/compaction/summary.ts +17 -3
  82. package/src/compaction/tool-result-editing.ts +12 -9
  83. package/src/manager/agent/lifecycle.ts +181 -1
  84. package/src/public-runtime.ts +2 -1
  85. package/src/runtime/query/checkpoint.ts +6 -1
  86. package/src/runtime/query/executor.ts +25 -1
  87. package/src/runtime/query/iteration/phases/compaction.ts +13 -2
  88. package/src/runtime/query/tool-output-budget.ts +65 -11
  89. package/src/scheduler/local.ts +14 -1
  90. package/src/store/evidence/disk.ts +28 -3
  91. package/src/store/evidence/source-text.ts +38 -2
  92. package/src/store/session-log/core.ts +28 -0
  93. package/src/store/session-log/spilled-input.ts +90 -0
  94. package/src/tools/builtins/read-file.ts +7 -2
  95. package/src/tools/retained-output.ts +38 -0
  96. package/src/types/agent/manager.ts +54 -0
  97. package/src/types/agent/scheduler.ts +21 -0
  98. package/src/types/agent/task.ts +2 -0
  99. package/src/types/session/records.ts +16 -0
  100. package/src/types/tool/index.ts +8 -0
@@ -51,6 +51,7 @@ import {
51
51
  isLeaseLive,
52
52
  } from './lease.js'
53
53
  import type { SpillReadOptions, SpillRef, SpillStore } from './spill.js'
54
+ import { TOOL_INPUT_RECORD_TYPES, spilledInput, toolInputSlots } from './spilled-input.js'
54
55
 
55
56
  // ─── public types ─────────────────────────────────────────────────────────
56
57
 
@@ -666,6 +667,9 @@ export class SessionLogCore implements SessionLog {
666
667
  ? await this.#spillStructuredResults(original)
667
668
  : original
668
669
  const type = candidate.type
670
+ if (typeof type === 'string' && TOOL_INPUT_RECORD_TYPES.has(type)) {
671
+ return this.#spillToolInputs(candidate)
672
+ }
669
673
  if (
670
674
  type !== 'message' &&
671
675
  type !== 'message_replaced' &&
@@ -715,6 +719,30 @@ export class SessionLogCore implements SessionLog {
715
719
  return { ...candidate, content: preview, spill: ref }
716
720
  }
717
721
 
722
+ /**
723
+ * A record that carries tool inputs and would not fit a line: each large
724
+ * input goes to `tool-results/` and the record keeps a stand-in, so the
725
+ * person is still asked and the call is still restored on resume.
726
+ */
727
+ async #spillToolInputs(candidate: Record<string, unknown>): Promise<Record<string, unknown>> {
728
+ const sizeOf = (record: Record<string, unknown>): number =>
729
+ Buffer.byteLength(`${JSON.stringify(record)}\n`, 'utf8')
730
+ if (sizeOf(candidate) <= this.#spillAbove) return candidate
731
+ const copy = structuredClone(candidate)
732
+ const slots = toolInputSlots(copy)
733
+ const bodies = slots.map(({ holder, key }) => JSON.stringify(holder[key]) ?? 'null')
734
+ // Largest first, and only as many as it takes to fit.
735
+ const order = slots.map((_, i) => i).sort((a, b) => bodies[b]!.length - bodies[a]!.length)
736
+ for (const index of order) {
737
+ const slot = slots[index]!
738
+ const body = bodies[index]!
739
+ const ref = await this.#spills.write(`input:${String(candidate.id)}:${index}`, 'text', body)
740
+ slot.holder[slot.key] = spilledInput(ref, body)
741
+ if (sizeOf(copy) <= this.#spillAbove) break
742
+ }
743
+ return copy
744
+ }
745
+
718
746
  async #spillStructuredResults(
719
747
  candidate: Record<string, unknown>,
720
748
  ): Promise<Record<string, unknown>> {
@@ -0,0 +1,90 @@
1
+ import type { SpillRef } from './spill.js'
2
+
3
+ /**
4
+ * A tool call's input can be larger than a session-log record may be (a
5
+ * write of a few megabytes, a batch of large writes). The record that carries
6
+ * it then holds this stand-in, and the input itself sits in `tool-results/`
7
+ * under the same rules as any other spilled body.
8
+ */
9
+ export const SPILLED_INPUT_KEY = '$spilledInput'
10
+
11
+ /** Characters of the input kept inline beside the reference. */
12
+ const INPUT_PREVIEW_CHARS = 200
13
+
14
+ export interface SpilledInput {
15
+ readonly [SPILLED_INPUT_KEY]: SpillRef
16
+ readonly preview: string
17
+ }
18
+
19
+ export function spilledInput(ref: SpillRef, json: string): SpilledInput {
20
+ return { [SPILLED_INPUT_KEY]: ref, preview: json.slice(0, INPUT_PREVIEW_CHARS) }
21
+ }
22
+
23
+ export function isSpilledInput(value: unknown): value is SpilledInput {
24
+ return (
25
+ typeof value === 'object' &&
26
+ value !== null &&
27
+ SPILLED_INPUT_KEY in value &&
28
+ typeof (value as Record<string, unknown>)[SPILLED_INPUT_KEY] === 'object'
29
+ )
30
+ }
31
+
32
+ /** Where a record keeps tool inputs: the holder object and the key of the input in it. */
33
+ export interface InputSlot {
34
+ readonly holder: Record<string, unknown>
35
+ readonly key: string
36
+ }
37
+
38
+ function callSlots(calls: unknown): InputSlot[] {
39
+ if (!Array.isArray(calls)) return []
40
+ const slots: InputSlot[] = []
41
+ for (const call of calls) {
42
+ if (typeof call === 'object' && call !== null && 'input' in call) {
43
+ slots.push({ holder: call as Record<string, unknown>, key: 'input' })
44
+ }
45
+ }
46
+ return slots
47
+ }
48
+
49
+ /** Record types that carry a tool call's whole input. */
50
+ export const TOOL_INPUT_RECORD_TYPES: ReadonlySet<string> = new Set([
51
+ 'tool_review_requested',
52
+ 'decision_requested',
53
+ 'tool_executing',
54
+ 'tool_input_completed',
55
+ ])
56
+
57
+ /**
58
+ * The input slots of one record. The record is read, never changed; the
59
+ * caller copies what it replaces.
60
+ */
61
+ export function toolInputSlots(record: Record<string, unknown>): InputSlot[] {
62
+ switch (record.type) {
63
+ case 'tool_review_requested':
64
+ return callSlots(record.toolCalls)
65
+ case 'decision_requested': {
66
+ const request = record.request as { toolCalls?: unknown } | undefined
67
+ return request ? callSlots(request.toolCalls) : []
68
+ }
69
+ case 'tool_executing':
70
+ case 'tool_input_completed':
71
+ return 'input' in record ? [{ holder: record, key: 'input' }] : []
72
+ default:
73
+ return []
74
+ }
75
+ }
76
+
77
+ /**
78
+ * The input slots of a request read back from a log, with any stand-in
79
+ * replaced by the input it stands for. Mutates and returns `request`.
80
+ */
81
+ export async function restoreSpilledRequestInputs<T extends { toolCalls?: unknown }>(
82
+ request: T,
83
+ read: (ref: SpillRef) => Promise<string>,
84
+ ): Promise<T> {
85
+ for (const { holder, key } of callSlots(request.toolCalls)) {
86
+ const value = holder[key]
87
+ if (isSpilledInput(value)) holder[key] = JSON.parse(await read(value[SPILLED_INPUT_KEY]))
88
+ }
89
+ return request
90
+ }
@@ -5,6 +5,7 @@ import type { ToolContext } from '../../types/tool/index.js'
5
5
  import type { ToolFacts } from '../../types/tool/presentation.js'
6
6
  import { defineTool } from '../defineTool.js'
7
7
  import { resolveWithinAnyReal, toolRoots } from '../paths.js'
8
+ import { resolveRetainedOutput } from '../retained-output.js'
8
9
  import { looksBinary } from '../text-window.js'
9
10
  import { fingerprintContent } from './content-fingerprint.js'
10
11
  import { type RenderedRead, renderCappedRead } from './read-render.js'
@@ -71,8 +72,12 @@ export const ReadFileTool = defineTool({
71
72
  },
72
73
 
73
74
  async execute(input, context) {
75
+ // This session's own retained tool output lives on the host, outside the
76
+ // roots and outside any sandbox. It is read here, as a plain window of text,
77
+ // because the result that named it told the model to.
78
+ const retained = await resolveRetainedOutput(context.retainedOutputDirectory, input.path)
74
79
  // Sandbox-aware: route through sandbox.readFile() when available
75
- if (context.sandbox) {
80
+ if (context.sandbox && retained === undefined) {
76
81
  const buffer = await context.sandbox.readFile(input.path)
77
82
  const binaryGuidance = describeStructuredBinaryRead(input.path, buffer)
78
83
  if (binaryGuidance) {
@@ -112,7 +117,7 @@ export const ReadFileTool = defineTool({
112
117
  }
113
118
  }
114
119
 
115
- const filePath = await resolveWithinAnyReal(toolRoots(context), input.path)
120
+ const filePath = retained ?? (await resolveWithinAnyReal(toolRoots(context), input.path))
116
121
  // Refused on the size alone, before the file is in memory, when no window
117
122
  // was named: the cap exists so a huge file is never loaded to be refused.
118
123
  if (input.readRange === undefined && input.limit === undefined) {
@@ -0,0 +1,38 @@
1
+ import { lstat, realpath } from 'node:fs/promises'
2
+ import { dirname, extname, isAbsolute, relative, resolve } from 'node:path'
3
+
4
+ function inside(root: string, path: string): boolean {
5
+ const rel = relative(root, path)
6
+ return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel)
7
+ }
8
+
9
+ /**
10
+ * The real path of a retained tool-output file the model may read, or
11
+ * `undefined` when `candidate` is not one.
12
+ *
13
+ * Only this session's own retained directory counts. The file must be an
14
+ * absolute path to a regular `.txt` file that is not a symbolic link, has no
15
+ * other hard link, and whose real location is inside the directory's real
16
+ * location (so a link anywhere along the path, or `..`, takes it out). Nothing
17
+ * is created, and nothing but a read is offered.
18
+ */
19
+ export async function resolveRetainedOutput(
20
+ directory: string | undefined,
21
+ candidate: string | undefined,
22
+ ): Promise<string | undefined> {
23
+ if (!directory || !candidate || !isAbsolute(candidate) || candidate.includes('\0'))
24
+ return undefined
25
+ try {
26
+ const base = await realpath(directory)
27
+ const lexical = resolve(candidate)
28
+ if (extname(lexical).toLowerCase() !== '.txt') return undefined
29
+ if (!inside(resolve(directory), lexical) && !inside(base, lexical)) return undefined
30
+ const stat = await lstat(lexical)
31
+ if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1) return undefined
32
+ const real = await realpath(lexical)
33
+ if (!inside(base, real) || dirname(real) === real) return undefined
34
+ return real
35
+ } catch {
36
+ return undefined
37
+ }
38
+ }
@@ -5,6 +5,48 @@ import type { SessionEventListener } from '../session/events.js'
5
5
  import type { AgentLifecycleListener } from './lifecycle-event.js'
6
6
  import type { AgentTask, AgentTaskContext, AgentTaskState, SendMessageOptions } from './task.js'
7
7
 
8
+ /** Why a saved child could not be made continuable again. */
9
+ export type RestoreChildRefusal =
10
+ /** No project layout is known, so there is no saved log to read. */
11
+ | 'no-layout'
12
+ /** Nothing readable is saved under that id for this parent. */
13
+ | 'not-saved'
14
+ /** The saved record belongs to another parent session. */
15
+ | 'foreign'
16
+ /** The saved record carries no continuation profile, was isolated, or names an agent the host cannot rebuild. */
17
+ | 'not-resumable'
18
+ /** The child never recorded an end: its process died mid-turn. */
19
+ | 'unsettled'
20
+ /** The child was cancelled. */
21
+ | 'stopped'
22
+ /** This manager already holds a continuation for it that is still running. */
23
+ | 'running'
24
+ /** Too many continuations are retained to take another. */
25
+ | 'full'
26
+
27
+ export interface RestoreChildOptions {
28
+ /** The saved child's session id. */
29
+ readonly childSessionId: SessionId
30
+ /** The directory the continued child works in; the same one it ran in. */
31
+ readonly workingDirectory: string
32
+ /**
33
+ * Which registered agent the child continues as, given the profile the host
34
+ * recorded at launch. Return `undefined` when the host cannot rebuild it.
35
+ */
36
+ readonly agentFor: (profile: Readonly<Record<string, string>>) => string | undefined
37
+ }
38
+
39
+ export type RestoreChildResult =
40
+ | {
41
+ readonly status: 'restored'
42
+ readonly childSessionId: SessionId
43
+ /** The agent chosen by {@link RestoreChildOptions.agentFor}; pass it to `createTask` with `resumeSessionId`. */
44
+ readonly agentId: string
45
+ readonly profile: Readonly<Record<string, string>>
46
+ readonly description: string
47
+ }
48
+ | { readonly status: 'refused'; readonly reason: RestoreChildRefusal }
49
+
8
50
  /**
9
51
  * Agent task lifecycle contract — task creation, cancellation, messaging, and completion tracking.
10
52
  * Concrete implementation: `AgentManager` in `manager/agent/lifecycle.ts`.
@@ -48,6 +90,18 @@ export interface AgentManagerContract {
48
90
  on(listener: AgentLifecycleListener): void
49
91
  off(listener: AgentLifecycleListener): void
50
92
 
93
+ /**
94
+ * Rebuild the right to continue a child that finished in an earlier process
95
+ * from its saved log and meta document, under the invoking parent's scope.
96
+ * Optional: only a manager that can read saved children offers it. Grants
97
+ * nothing by itself; continuing still goes through `sendMessage` with
98
+ * `resumeSessionId`, with the invoking turn's authority and limits.
99
+ */
100
+ restoreChildConversation?(
101
+ options: RestoreChildOptions,
102
+ context: AgentTaskContext,
103
+ ): Promise<RestoreChildResult>
104
+
51
105
  cleanup(): void
52
106
  dispose(): void
53
107
  }
@@ -5,6 +5,7 @@ import type { CancelCause } from '../session/cancel-cause.js'
5
5
  import type { ChildSessionLifecycleEvent, SessionEventListener } from '../session/events.js'
6
6
  import type { WorkspaceRef } from '../workspace/ref.js'
7
7
  import type { AgentRuntimeContext, BaseAgentConfig, BaseAgentResult } from './base.js'
8
+ import type { RestoreChildOptions, RestoreChildResult } from './manager.js'
8
9
  import type { AgentTaskState, ChildWorkspaceRequest } from './task.js'
9
10
 
10
11
  export interface TaskHandle {
@@ -41,6 +42,15 @@ export interface CreateTaskOptions {
41
42
  * and unchanged agent definition. A saved log alone grants no authority.
42
43
  */
43
44
  readonly resumeSessionId?: SessionId
45
+ /**
46
+ * What a later process needs to rebuild this child, written to the child's
47
+ * saved meta document: at most 16 string entries, keys up to 64 and values up
48
+ * to 256 characters. The host defines the keys (which agent to rebuild,
49
+ * which model) and reads them back from `restoreChild`. Never put a secret
50
+ * here; it lands on disk beside the log. Absent, the child cannot be
51
+ * continued from disk after its process ends.
52
+ */
53
+ readonly continuationProfile?: Readonly<Record<string, string>>
44
54
  /** Per-task filesystem choice, forwarded to the local manager at admission. */
45
55
  readonly workspace?: ChildWorkspaceRequest
46
56
  /**
@@ -156,6 +166,17 @@ export interface TaskScheduler {
156
166
 
157
167
  continueTask(taskId: TaskId, message: string): Promise<void>
158
168
 
169
+ /**
170
+ * Make a child saved by an earlier process continuable again, from its saved
171
+ * log and meta document, under this scheduler's parent scope. OPTIONAL: only
172
+ * a scheduler that can read saved children offers it. It runs nothing; the
173
+ * caller then continues the child with `createTask({ resumeSessionId })`, so
174
+ * the invoking turn's authority and limits apply. See
175
+ * {@link CreateTaskOptions.continuationProfile} for what must have been
176
+ * recorded at launch.
177
+ */
178
+ restoreChild?(options: RestoreChildOptions): Promise<RestoreChildResult>
179
+
159
180
  /**
160
181
  * Stop one task. A cause is relative to the child: a blocking delegation
161
182
  * abandoned by the turn that launched it is cancelled by its `parent`.
@@ -238,6 +238,8 @@ export interface AgentTask {
238
238
  export interface SendMessageOptions {
239
239
  /** See {@link import('./scheduler.js').CreateTaskOptions.resumeSessionId}. */
240
240
  readonly resumeSessionId?: SessionId
241
+ /** See {@link import('./scheduler.js').CreateTaskOptions.continuationProfile}. */
242
+ readonly continuationProfile?: Readonly<Record<string, string>>
241
243
  /** Explicit child filesystem policy; omitted preserves legacy backend behavior. */
242
244
  readonly workspace?: ChildWorkspaceRequest
243
245
  /**
@@ -1039,6 +1039,22 @@ export const ChildSessionMetaSchema = z
1039
1039
  status: turnExecutionStatus,
1040
1040
  createdAt: isoUtc,
1041
1041
  endedAt: isoUtc.optional(),
1042
+ /**
1043
+ * What a later process needs to continue this child from its saved log:
1044
+ * the filesystem policy it ran under and a small host-defined profile
1045
+ * (which agent to rebuild, which model). Absent on children saved before
1046
+ * this field existed and on children whose host recorded no profile; those
1047
+ * cannot be continued from disk.
1048
+ */
1049
+ continuation: z
1050
+ .object({
1051
+ workspace: z.enum(['shared', 'isolated']),
1052
+ profile: z
1053
+ .record(z.string().min(1).max(64), z.string().max(256))
1054
+ .refine((value) => Object.keys(value).length <= 16, 'At most 16 profile entries'),
1055
+ })
1056
+ .strict()
1057
+ .optional(),
1042
1058
  })
1043
1059
  .strict()
1044
1060
  export type ChildSessionMeta = z.infer<typeof ChildSessionMetaSchema>
@@ -458,6 +458,14 @@ export interface ToolContext {
458
458
  * directory; an absolute path inside any of these is accepted.
459
459
  */
460
460
  additionalDirectories?: readonly string[]
461
+ /**
462
+ * Absolute path of the directory holding THIS session's retained tool
463
+ * output (the files an oversized result was saved to). The `read` tool may
464
+ * open a regular file in it, and only that, without the path being a
465
+ * reviewed or added directory: the model was already shown the path in the
466
+ * result it came from. Set by the executor whenever the turn retains output.
467
+ */
468
+ retainedOutputDirectory?: string
461
469
  /**
462
470
  * Absolute paths outside the working directory and the added directories
463
471
  * that THIS call was approved to reach.