@namzu/sdk 20.2.0 → 20.3.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 (49) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/manager/run/persistence.d.ts +2 -2
  3. package/dist/manager/run/persistence.d.ts.map +1 -1
  4. package/dist/manager/run/persistence.js +14 -5
  5. package/dist/manager/run/persistence.js.map +1 -1
  6. package/dist/public-runtime.d.ts +1 -1
  7. package/dist/public-runtime.d.ts.map +1 -1
  8. package/dist/public-runtime.js +1 -1
  9. package/dist/public-runtime.js.map +1 -1
  10. package/dist/runtime/query/context.d.ts +2 -0
  11. package/dist/runtime/query/context.d.ts.map +1 -1
  12. package/dist/runtime/query/context.js +1 -0
  13. package/dist/runtime/query/context.js.map +1 -1
  14. package/dist/runtime/query/index.d.ts +11 -0
  15. package/dist/runtime/query/index.d.ts.map +1 -1
  16. package/dist/runtime/query/index.js +1 -0
  17. package/dist/runtime/query/index.js.map +1 -1
  18. package/dist/store/index.d.ts +1 -0
  19. package/dist/store/index.d.ts.map +1 -1
  20. package/dist/store/index.js +1 -0
  21. package/dist/store/index.js.map +1 -1
  22. package/dist/store/run/disk.d.ts +10 -8
  23. package/dist/store/run/disk.d.ts.map +1 -1
  24. package/dist/store/run/disk.js.map +1 -1
  25. package/dist/store/run/memory.d.ts +46 -0
  26. package/dist/store/run/memory.d.ts.map +1 -0
  27. package/dist/store/run/memory.js +104 -0
  28. package/dist/store/run/memory.js.map +1 -0
  29. package/dist/types/run/config.d.ts +12 -0
  30. package/dist/types/run/config.d.ts.map +1 -1
  31. package/dist/types/run/index.d.ts +1 -0
  32. package/dist/types/run/index.d.ts.map +1 -1
  33. package/dist/types/run/index.js +1 -0
  34. package/dist/types/run/index.js.map +1 -1
  35. package/dist/types/run/store.d.ts +103 -0
  36. package/dist/types/run/store.d.ts.map +1 -0
  37. package/dist/types/run/store.js +30 -0
  38. package/dist/types/run/store.js.map +1 -0
  39. package/package.json +1 -1
  40. package/src/manager/run/persistence.ts +17 -7
  41. package/src/public-runtime.ts +1 -0
  42. package/src/runtime/query/context.ts +3 -0
  43. package/src/runtime/query/index.ts +13 -0
  44. package/src/store/index.ts +1 -0
  45. package/src/store/run/disk.ts +10 -8
  46. package/src/store/run/memory.ts +121 -0
  47. package/src/types/run/config.ts +13 -0
  48. package/src/types/run/index.ts +1 -0
  49. package/src/types/run/store.ts +112 -0
@@ -0,0 +1,121 @@
1
+ import type { Run } from '../../types/run/entity.js'
2
+ import type { RunEvent } from '../../types/run/events.js'
3
+ import type { CompletedToolRecord, RunStore } from '../../types/run/store.js'
4
+
5
+ /**
6
+ * Process-local {@link RunStore}: a run's evidence with no filesystem.
7
+ *
8
+ * The reason it ships rather than living in a test file is that it is the
9
+ * only way to demonstrate the seam actually is one. A contract with a single
10
+ * implementation is a refactor; the second implementation is what proves a
11
+ * host could supply a third. It is also the parity partner for the disk
12
+ * store — a memory store that answers differently from disk is worse than
13
+ * none, because a host tests against one and ships the other.
14
+ *
15
+ * Deliberately not durable. It is for tests, for a single-process host that
16
+ * genuinely wants a run's evidence to die with the process, and for
17
+ * environments with no writable filesystem at all.
18
+ */
19
+ export class InMemoryRunStore implements RunStore {
20
+ private runId: string | null = null
21
+ private parentRunId: string | undefined
22
+ private meta: Run | null = null
23
+ private messages: Run['messages'] = []
24
+ private report: string | null = null
25
+ private readonly events: RunEvent[] = []
26
+
27
+ async initRun(runId: string, parentRunId?: string): Promise<string | null> {
28
+ this.runId = runId
29
+ this.parentRunId = parentRunId
30
+ // No location, and that is the honest answer rather than a defect.
31
+ // Callers render `null` as "this run is not on a filesystem"; a
32
+ // synthesized path would put a directory that does not exist in front
33
+ // of an operator.
34
+ return null
35
+ }
36
+
37
+ private requireInit(): string {
38
+ if (this.runId === null) {
39
+ throw new Error('InMemoryRunStore not initialized — call initRun() first')
40
+ }
41
+ return this.runId
42
+ }
43
+
44
+ /** The run this store is bound to, and its parent when it has one. */
45
+ get boundTo(): { runId: string; parentRunId?: string } | null {
46
+ return this.runId === null
47
+ ? null
48
+ : { runId: this.runId, ...(this.parentRunId ? { parentRunId: this.parentRunId } : {}) }
49
+ }
50
+
51
+ async writeRunMeta(run: Run): Promise<void> {
52
+ this.requireInit()
53
+ // Copied, not referenced. The caller keeps mutating this object for
54
+ // the rest of the run, so storing it by reference would make every
55
+ // historical read return the run's present state — a transcript that
56
+ // silently rewrites itself is worse than no transcript.
57
+ this.meta = structuredClone(run)
58
+ }
59
+
60
+ async writeMessages(run: Run): Promise<void> {
61
+ this.requireInit()
62
+ this.messages = structuredClone(run.messages)
63
+ }
64
+
65
+ async appendEvent(event: RunEvent): Promise<void> {
66
+ this.requireInit()
67
+ // Stamped on write, exactly as the disk store stamps its transcript
68
+ // line — a parity test compares the two read-backs, and a timestamp
69
+ // present in one medium and absent in the other would make identical
70
+ // runs look different depending on where they were recorded.
71
+ this.events.push({ ...event, timestamp: Date.now() } as unknown as RunEvent)
72
+ }
73
+
74
+ async writeReport(content: string): Promise<string | null> {
75
+ this.requireInit()
76
+ this.report = content
77
+ return null
78
+ }
79
+
80
+ async readCompletedTools(): Promise<Map<string, CompletedToolRecord>> {
81
+ this.requireInit()
82
+ const completed = new Map<string, CompletedToolRecord>()
83
+ for (const event of this.events) {
84
+ const e = event as unknown as Record<string, unknown>
85
+ if (e.type !== 'tool_completed') continue
86
+ const toolUseId = e.toolUseId
87
+ const toolName = e.toolName
88
+ if (typeof toolUseId !== 'string' || typeof toolName !== 'string') continue
89
+ // Last write wins: a retried tool emits one event per attempt and
90
+ // the final one is what actually answered the call. Same rule the
91
+ // disk store applies, and it has to be the same rule — a resumed
92
+ // run must not depend on which backend it was recorded with.
93
+ completed.set(toolUseId, {
94
+ toolUseId,
95
+ toolName,
96
+ result: typeof e.result === 'string' ? e.result : '',
97
+ isError: e.isError === true,
98
+ })
99
+ }
100
+ return completed
101
+ }
102
+
103
+ getRunDir(): string | null {
104
+ return null
105
+ }
106
+
107
+ // `addToIndex` is deliberately not implemented. It maintains a browsable
108
+ // catalogue for a human reading a directory, and there is no directory
109
+ // here. The optional method exists on the contract precisely so a backend
110
+ // can decline it rather than implement a no-op that looks like a listing.
111
+
112
+ /** Everything recorded for the bound run, for tests and parity checks. */
113
+ snapshot(): {
114
+ meta: Run | null
115
+ messages: Run['messages']
116
+ report: string | null
117
+ events: readonly RunEvent[]
118
+ } {
119
+ return { meta: this.meta, messages: this.messages, report: this.report, events: this.events }
120
+ }
121
+ }
@@ -136,6 +136,19 @@ export interface RunPersistenceConfig {
136
136
  * hosts inject a scope-keyed backend (e.g. Postgres) here.
137
137
  */
138
138
  checkpointStore?: CheckpointStore
139
+
140
+ /**
141
+ * Optional run-evidence persistence override. Defaults to the disk layout
142
+ * under `outputDir` (a
143
+ * {@link import('../../store/run/disk.js').RunDiskStore}); hosts inject
144
+ * their own backend here.
145
+ *
146
+ * The sibling of `checkpointStore`, and it should have been one from the
147
+ * start: checkpoints got an injectable seam while the run record, its
148
+ * messages, its transcript and its report did not, so the evidence was
149
+ * the one part of a run that could not leave the local filesystem.
150
+ */
151
+ runStore?: import('./store.js').RunStore
139
152
  }
140
153
 
141
154
  export interface RunStoreConfig {
@@ -3,6 +3,7 @@ export * from './prepare-step.js'
3
3
  export * from './stop-reason.js'
4
4
  export * from './config.js'
5
5
  export * from './checkpoint-store.js'
6
+ export * from './store.js'
6
7
  export * from './entity.js'
7
8
  export * from './replay.js'
8
9
  // Domain `RunStatus` (session-hierarchy.md §4.6 state machine). Safe to
@@ -0,0 +1,112 @@
1
+ /**
2
+ * RunStore — persistence contract for a run's own evidence.
3
+ *
4
+ * The checkpoint store got an injectable seam and this did not, which left
5
+ * the run record, its messages, its transcript and its report reachable only
6
+ * through a concrete filesystem class. For a kernel whose stated purpose is
7
+ * auditable evidence, the evidence was the one thing that could not be
8
+ * pointed at durable storage: on ephemeral infrastructure the transcript dies
9
+ * with the container, and behind a load balancer two replicas write two
10
+ * disjoint run trees for one tenant.
11
+ *
12
+ * The location was already injectable through a path builder — but that
13
+ * returns filesystem path strings, so it relocates the directory without
14
+ * changing the medium.
15
+ *
16
+ * ## Bound to one run, unlike {@link CheckpointStore}
17
+ *
18
+ * Every accessor here addresses the run the store was bound to by
19
+ * {@link RunStore.initRun}, where a `CheckpointStore` takes an explicit scope
20
+ * per call. That asymmetry is inherited rather than chosen: this contract is
21
+ * extracted from a class the runtime already constructs per run and holds for
22
+ * the run's lifetime, and re-keying it would change every call site in the
23
+ * same change that introduces the seam — two risks where one will do.
24
+ *
25
+ * A host implementing a shared backend therefore keys its rows by the
26
+ * attribution it was constructed with plus the bound run id. If this is later
27
+ * re-keyed per call, it happens once, deliberately, as its own change.
28
+ */
29
+
30
+ import type { Run } from './entity.js'
31
+ import type { RunEvent } from './events.js'
32
+
33
+ /**
34
+ * One finished tool call, recovered from the run's own transcript.
35
+ *
36
+ * Re-declared here rather than imported from the disk store so the contract
37
+ * does not depend on an implementation of itself.
38
+ */
39
+ export interface CompletedToolRecord {
40
+ readonly toolUseId: string
41
+ readonly toolName: string
42
+ readonly result: string
43
+ readonly isError: boolean
44
+ }
45
+
46
+ export interface RunStore {
47
+ /**
48
+ * Bind this store to a run, before any other call.
49
+ *
50
+ * Returns a location when the backend has one — the built-in disk store
51
+ * returns the run's directory — and `null` when it does not. A caller
52
+ * that renders the value must treat `null` as "this run is not on a
53
+ * filesystem" rather than as an error: an in-memory or object-storage
54
+ * backend has nothing to print, and inventing a path for it would put a
55
+ * directory that does not exist in front of an operator.
56
+ */
57
+ initRun(runId: string, parentRunId?: string): Promise<string | null>
58
+
59
+ /** Persist the run record: status, metadata, usage, timings. */
60
+ writeRunMeta(run: Run): Promise<void>
61
+
62
+ /** Persist the run's full message history. */
63
+ writeMessages(run: Run): Promise<void>
64
+
65
+ /**
66
+ * Append one event to the run's durable event log.
67
+ *
68
+ * High-frequency streaming deltas are excluded before they reach here —
69
+ * that exclusion is a deliberate trade and belongs to the emitter, not to
70
+ * the backend, so a store must not re-filter.
71
+ */
72
+ appendEvent(event: RunEvent): Promise<void>
73
+
74
+ /**
75
+ * Persist the run's final report. Returns a location, or `null` when the
76
+ * backend has none. See {@link RunStore.initRun}.
77
+ */
78
+ writeReport(content: string): Promise<string | null>
79
+
80
+ /**
81
+ * Every tool call this run has already finished, keyed by `toolUseId`.
82
+ *
83
+ * A batch's results reach the message history only once the WHOLE batch
84
+ * settles, so a hard kill part-way through loses every result that had
85
+ * already come back, and the resumed run re-executes those calls. For a
86
+ * file write that is waste; for a payment or an email it is a second one.
87
+ *
88
+ * A backend that does not retain individual events answers with an empty
89
+ * map, which costs re-execution and is honest. It must not answer with a
90
+ * PARTIAL map: a caller reads a present entry as "this call is already
91
+ * answered", so a half-remembered batch is worse than a forgotten one.
92
+ */
93
+ readCompletedTools(): Promise<Map<string, CompletedToolRecord>>
94
+
95
+ /**
96
+ * Where this run's evidence lives, or `null` when it is not on a
97
+ * filesystem. Valid only after {@link RunStore.initRun}.
98
+ */
99
+ getRunDir(): string | null
100
+
101
+ /**
102
+ * Record the run in a browsable catalogue of runs. OPTIONAL.
103
+ *
104
+ * Optional because it is the one method here that is not evidence: it
105
+ * maintains a convenience listing for a human reading the directory, and
106
+ * a backend whose runs are already queryable has nothing to add. The
107
+ * programmatic answer to "which runs are there" is
108
+ * `CheckpointStore.listDurableRuns`, which carries attribution and
109
+ * includes sub-runs; this does neither.
110
+ */
111
+ addToIndex?(run: Run): Promise<void>
112
+ }