@graphorin/core 0.6.0 → 0.7.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 (112) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +14 -8
  3. package/dist/channels/channels.d.ts.map +1 -1
  4. package/dist/channels/index.d.ts +2 -2
  5. package/dist/channels/index.js +2 -2
  6. package/dist/channels/pause.d.ts +47 -2
  7. package/dist/channels/pause.d.ts.map +1 -1
  8. package/dist/channels/pause.js +62 -2
  9. package/dist/channels/pause.js.map +1 -1
  10. package/dist/contracts/checkpoint-store.d.ts +97 -1
  11. package/dist/contracts/checkpoint-store.d.ts.map +1 -1
  12. package/dist/contracts/checkpoint-store.js.map +1 -1
  13. package/dist/contracts/index.d.ts +5 -5
  14. package/dist/contracts/index.js +2 -1
  15. package/dist/contracts/memory-store.d.ts +59 -2
  16. package/dist/contracts/memory-store.d.ts.map +1 -1
  17. package/dist/contracts/provider.d.ts +20 -6
  18. package/dist/contracts/provider.d.ts.map +1 -1
  19. package/dist/contracts/session-store.d.ts +10 -2
  20. package/dist/contracts/session-store.d.ts.map +1 -1
  21. package/dist/contracts/tool.d.ts +75 -1
  22. package/dist/contracts/tool.d.ts.map +1 -1
  23. package/dist/contracts/tool.js +57 -0
  24. package/dist/contracts/tool.js.map +1 -0
  25. package/dist/contracts/tracer.d.ts +53 -5
  26. package/dist/contracts/tracer.d.ts.map +1 -1
  27. package/dist/contracts/tracer.js.map +1 -1
  28. package/dist/index.d.ts +14 -9
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +11 -7
  31. package/dist/index.js.map +1 -1
  32. package/dist/package.js +6 -0
  33. package/dist/package.js.map +1 -0
  34. package/dist/types/agent-event-wire.d.ts +74 -0
  35. package/dist/types/agent-event-wire.d.ts.map +1 -0
  36. package/dist/types/agent-event-wire.js +130 -0
  37. package/dist/types/agent-event-wire.js.map +1 -0
  38. package/dist/types/agent-event.d.ts +48 -6
  39. package/dist/types/agent-event.d.ts.map +1 -1
  40. package/dist/types/index.d.ts +3 -2
  41. package/dist/types/index.js +2 -1
  42. package/dist/types/memory.d.ts +11 -0
  43. package/dist/types/memory.d.ts.map +1 -1
  44. package/dist/types/run.d.ts +86 -4
  45. package/dist/types/run.d.ts.map +1 -1
  46. package/dist/types/run.js.map +1 -1
  47. package/dist/types/tool.d.ts +10 -0
  48. package/dist/types/tool.d.ts.map +1 -1
  49. package/dist/types/usage.d.ts +11 -1
  50. package/dist/types/usage.d.ts.map +1 -1
  51. package/dist/types/usage.js.map +1 -1
  52. package/dist/utils/binary-json.d.ts +165 -0
  53. package/dist/utils/binary-json.d.ts.map +1 -0
  54. package/dist/utils/binary-json.js +240 -0
  55. package/dist/utils/binary-json.js.map +1 -0
  56. package/dist/utils/index.d.ts +2 -1
  57. package/dist/utils/index.js +2 -1
  58. package/dist/utils/validation.d.ts +10 -1
  59. package/dist/utils/validation.d.ts.map +1 -1
  60. package/dist/utils/validation.js +1 -1
  61. package/dist/utils/validation.js.map +1 -1
  62. package/package.json +9 -7
  63. package/src/channels/channels.ts +206 -0
  64. package/src/channels/directive.ts +41 -0
  65. package/src/channels/dispatch.ts +37 -0
  66. package/src/channels/durable.ts +151 -0
  67. package/src/channels/index.ts +62 -0
  68. package/src/channels/pause.ts +216 -0
  69. package/src/contracts/auth-token-store.ts +42 -0
  70. package/src/contracts/checkpoint-store.ts +256 -0
  71. package/src/contracts/embedder.ts +42 -0
  72. package/src/contracts/eval-scorer.ts +44 -0
  73. package/src/contracts/index.ts +112 -0
  74. package/src/contracts/local-provider-trust.ts +33 -0
  75. package/src/contracts/logger.ts +61 -0
  76. package/src/contracts/memory-store.ts +187 -0
  77. package/src/contracts/oauth-server-store.ts +78 -0
  78. package/src/contracts/preferred-model.ts +56 -0
  79. package/src/contracts/provider.ts +316 -0
  80. package/src/contracts/reasoning-retention.ts +52 -0
  81. package/src/contracts/redaction-validator.ts +56 -0
  82. package/src/contracts/sandbox.ts +70 -0
  83. package/src/contracts/secret-ref.ts +22 -0
  84. package/src/contracts/secret-value.ts +117 -0
  85. package/src/contracts/secrets-store.ts +90 -0
  86. package/src/contracts/session-store.ts +163 -0
  87. package/src/contracts/token-counter.ts +23 -0
  88. package/src/contracts/tool.ts +397 -0
  89. package/src/contracts/tracer.ts +219 -0
  90. package/src/contracts/trigger-store.ts +40 -0
  91. package/src/index.ts +23 -0
  92. package/src/types/agent-event-wire.ts +193 -0
  93. package/src/types/agent-event.ts +579 -0
  94. package/src/types/handoff.ts +111 -0
  95. package/src/types/index.ts +148 -0
  96. package/src/types/memory.ts +427 -0
  97. package/src/types/message.ts +174 -0
  98. package/src/types/run.ts +312 -0
  99. package/src/types/sensitivity.ts +35 -0
  100. package/src/types/session-scope.ts +18 -0
  101. package/src/types/stop-condition.ts +108 -0
  102. package/src/types/tool-call.ts +24 -0
  103. package/src/types/tool.ts +324 -0
  104. package/src/types/usage.ts +120 -0
  105. package/src/types/workflow-event.ts +132 -0
  106. package/src/utils/assert-never.ts +24 -0
  107. package/src/utils/async-context.ts +55 -0
  108. package/src/utils/binary-json.ts +425 -0
  109. package/src/utils/hash.ts +122 -0
  110. package/src/utils/index.ts +57 -0
  111. package/src/utils/streams.ts +233 -0
  112. package/src/utils/validation.ts +82 -0
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Workflow channel kinds + control-flow primitives.
3
+ *
4
+ * Names are Graphorin's own design (`Directive`, `Dispatch`, `pause`,
5
+ * `LatestValue`, `Reducer`, `Stream`, `Barrier`, `Ephemeral`, `AnyValue`)
6
+ * and must not be aliased to terms from other workflow libraries.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ export type {
12
+ AnyValue,
13
+ Barrier,
14
+ Channel,
15
+ ChannelKind,
16
+ Ephemeral,
17
+ LatestValue,
18
+ ListAggregate,
19
+ Reducer,
20
+ Stream,
21
+ } from './channels.js';
22
+ export {
23
+ anyValue,
24
+ barrier,
25
+ ephemeral,
26
+ latestValue,
27
+ listAggregate,
28
+ reducer,
29
+ stream,
30
+ } from './channels.js';
31
+
32
+ export type { DirectiveOptions } from './directive.js';
33
+ export { Directive } from './directive.js';
34
+ export { Dispatch, dispatch } from './dispatch.js';
35
+ export type {
36
+ ApprovalPauseValue,
37
+ AwakeablePauseValue,
38
+ TimerPauseValue,
39
+ } from './durable.js';
40
+ export {
41
+ APPROVAL_PAUSE_KIND,
42
+ AWAKEABLE_PAUSE_KIND,
43
+ awaitExternal,
44
+ isApprovalPauseValue,
45
+ isAwakeablePauseValue,
46
+ isTimerPauseValue,
47
+ requestApproval,
48
+ sleepFor,
49
+ sleepUntil,
50
+ TIMER_PAUSE_KIND,
51
+ } from './durable.js';
52
+ export type { PauseIdentity, PauseResumeScope } from './pause.js';
53
+ export {
54
+ isPauseSignal,
55
+ isReplayDivergenceSignal,
56
+ PAUSE_SIGNAL_BRAND,
57
+ PauseSignal,
58
+ pause,
59
+ REPLAY_DIVERGENCE_BRAND,
60
+ ReplayDivergenceSignal,
61
+ runWithPauseResume,
62
+ } from './pause.js';
@@ -0,0 +1,216 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+
3
+ /**
4
+ * Brand attached to the signal thrown by `pause(value)` so that the
5
+ * workflow runtime can recognise it across realms (Worker threads,
6
+ * sandboxes, …) without `instanceof`.
7
+ *
8
+ * @stable
9
+ */
10
+ export const PAUSE_SIGNAL_BRAND: unique symbol = Symbol.for('graphorin.PauseSignal');
11
+
12
+ /**
13
+ * Thrown by `pause(value)` from inside a workflow node. The runtime
14
+ * catches it, persists state with a pending pause, and suspends the
15
+ * thread until `Workflow.resume(threadId, directive)` is called.
16
+ *
17
+ * Application code should never construct or catch this directly -
18
+ * always go through `pause(...)`.
19
+ *
20
+ * @stable
21
+ */
22
+ export class PauseSignal<TValue = unknown> extends Error {
23
+ readonly [PAUSE_SIGNAL_BRAND]: true = true;
24
+ readonly value: TValue;
25
+
26
+ constructor(value: TValue) {
27
+ super('graphorin: workflow paused');
28
+ this.name = 'PauseSignal';
29
+ this.value = value;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Brand attached to the signal thrown by `pause(value)` when the
35
+ * positional replay diverges from the journaled pause identity
36
+ * (W-120). Cross-realm safe like {@link PAUSE_SIGNAL_BRAND}.
37
+ *
38
+ * @stable
39
+ */
40
+ export const REPLAY_DIVERGENCE_BRAND: unique symbol = Symbol.for(
41
+ 'graphorin.ReplayDivergenceSignal',
42
+ );
43
+
44
+ /**
45
+ * Identity of one pause as recorded next to its satisfied resume value
46
+ * (W-120): the durable-primitive `kind` (`timer` / `awakeable` /
47
+ * `approval`) and the awakeable/approval `name`. A plain `pause()` has
48
+ * neither - two plain pauses are indistinguishable BY DESIGN (no
49
+ * false positives; the check is deliberately conservative).
50
+ *
51
+ * @stable
52
+ */
53
+ export interface PauseIdentity {
54
+ readonly kind?: string;
55
+ readonly name?: string;
56
+ }
57
+
58
+ /**
59
+ * Thrown by `pause(value)` during replay when the CURRENT pause's
60
+ * identity does not match what the journal recorded for this cursor
61
+ * position (W-120): the node body's pause order depends on
62
+ * time/state/LLM output, so a positional replay would silently hand a
63
+ * resume value to the wrong pause. The workflow engine converts this
64
+ * into a typed `pause-replay-divergence` WorkflowError.
65
+ *
66
+ * @stable
67
+ */
68
+ export class ReplayDivergenceSignal extends Error {
69
+ readonly [REPLAY_DIVERGENCE_BRAND]: true = true;
70
+ readonly expected: PauseIdentity;
71
+ readonly actual: PauseIdentity;
72
+ readonly cursor: number;
73
+
74
+ constructor(expected: PauseIdentity, actual: PauseIdentity, cursor: number) {
75
+ super(
76
+ `graphorin: pause replay divergence at cursor ${cursor}: paused as ${describeIdentity(actual)} where the journal recorded ${describeIdentity(expected)}`,
77
+ );
78
+ this.name = 'ReplayDivergenceSignal';
79
+ this.expected = expected;
80
+ this.actual = actual;
81
+ this.cursor = cursor;
82
+ }
83
+ }
84
+
85
+ /** Cross-realm safe type guard for {@link ReplayDivergenceSignal}. @stable */
86
+ export function isReplayDivergenceSignal(err: unknown): err is ReplayDivergenceSignal {
87
+ return (
88
+ typeof err === 'object' &&
89
+ err !== null &&
90
+ (err as Record<symbol, unknown>)[REPLAY_DIVERGENCE_BRAND] === true
91
+ );
92
+ }
93
+
94
+ function describeIdentity(id: PauseIdentity): string {
95
+ const parts: string[] = [];
96
+ if (id.kind !== undefined) parts.push(`kind '${id.kind}'`);
97
+ if (id.name !== undefined) parts.push(`name '${id.name}'`);
98
+ return parts.length > 0 ? parts.join(' / ') : 'a plain pause';
99
+ }
100
+
101
+ /**
102
+ * Extract the {@link PauseIdentity} of a pause payload: generic field
103
+ * access only (no import of the durable-primitive types - that would
104
+ * cycle).
105
+ */
106
+ function identityOfPauseValue(value: unknown): PauseIdentity {
107
+ if (typeof value !== 'object' || value === null) return {};
108
+ const v = value as { readonly kind?: unknown; readonly name?: unknown };
109
+ return {
110
+ ...(typeof v.kind === 'string' ? { kind: v.kind } : {}),
111
+ ...(typeof v.name === 'string' ? { name: v.name } : {}),
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Resume-injection scope set by the workflow runtime around the second
117
+ * (and later) invocations of a paused node body. When the scope is
118
+ * present, `pause(...)` consults it to decide whether to throw a fresh
119
+ * {@link PauseSignal} or return the injected value the runtime supplied
120
+ * via `Workflow.resume(threadId, new Directive({ resume }))`.
121
+ *
122
+ * This is the storage mechanism that gives `pause()` its symmetric
123
+ * pair semantics (`pause` ↔ `resume`) without forcing every node body
124
+ * to be re-architected as a state machine.
125
+ *
126
+ * @internal
127
+ */
128
+ export interface PauseResumeScope {
129
+ /** Ordered resume values replayed to successive `pause()` calls (WF-2). */
130
+ readonly values: ReadonlyArray<unknown>;
131
+ /**
132
+ * W-120: per-value identity of the pause each value answered.
133
+ * Absent (legacy checkpoints) or `null`/empty entries skip the check.
134
+ */
135
+ readonly meta?: ReadonlyArray<PauseIdentity | null | undefined>;
136
+ cursor: number;
137
+ }
138
+
139
+ const pauseResumeStorage = new AsyncLocalStorage<PauseResumeScope>();
140
+
141
+ /**
142
+ * Run `fn` inside a scope where successive `pause(...)` calls return the
143
+ * supplied `values` in order instead of throwing a fresh
144
+ * {@link PauseSignal} (WF-2: a node body re-executes from the top on
145
+ * every resume, so earlier pauses must replay their already-delivered
146
+ * values and only the FIRST unsatisfied `pause()` suspends again). An
147
+ * empty `values` array behaves exactly like no scope - every `pause()`
148
+ * suspends - which is what a static-gate resume needs so a programmatic
149
+ * `pause()` inside the node is never silently satisfied.
150
+ *
151
+ * This helper is the contract between the runtime and `pause(...)`.
152
+ * Consumers of `pause(...)` never call it directly - only the workflow
153
+ * engine wires it up around the resumed node body.
154
+ *
155
+ * @internal
156
+ */
157
+ export function runWithPauseResume<R>(
158
+ values: ReadonlyArray<unknown>,
159
+ fn: () => R | Promise<R>,
160
+ meta?: ReadonlyArray<PauseIdentity | null | undefined>,
161
+ ): Promise<R> {
162
+ const scope: PauseResumeScope = { values, ...(meta !== undefined ? { meta } : {}), cursor: 0 };
163
+ return pauseResumeStorage.run(scope, async () => fn());
164
+ }
165
+
166
+ /**
167
+ * Programmatically suspend the current workflow node. The `value` is
168
+ * surfaced to callers via the `WorkflowSuspendedEvent.value` field; the
169
+ * eventual `Directive({ resume })` is delivered as the return value of
170
+ * this call once the runtime resumes the thread.
171
+ *
172
+ * Implementation note: when the call is made outside a runtime-managed
173
+ * resume scope, `pause(...)` throws a fresh {@link PauseSignal} so the
174
+ * engine can catch it, persist state, and suspend. When the runtime
175
+ * later resumes the node body, it wraps the second invocation in
176
+ * {@link runWithPauseResume}, which causes the same `pause(...)` call to
177
+ * return the operator-supplied resume value instead of throwing.
178
+ *
179
+ * @stable
180
+ */
181
+ export function pause<TValue, TResume = unknown>(value: TValue): TResume {
182
+ const scope = pauseResumeStorage.getStore();
183
+ if (scope !== undefined && scope.cursor < scope.values.length) {
184
+ // W-120: verify the replayed value is answering the SAME pause the
185
+ // journal recorded at this cursor. Legacy checkpoints (no meta) and
186
+ // plain pauses (empty identity) replay unchecked - conservative by
187
+ // design, false positives are impossible.
188
+ const expected = scope.meta?.[scope.cursor];
189
+ if (expected != null && (expected.kind !== undefined || expected.name !== undefined)) {
190
+ const actual = identityOfPauseValue(value);
191
+ if (
192
+ (expected.kind !== undefined && expected.kind !== actual.kind) ||
193
+ (expected.name !== undefined && expected.name !== actual.name)
194
+ ) {
195
+ throw new ReplayDivergenceSignal(expected, actual, scope.cursor);
196
+ }
197
+ }
198
+ const next = scope.values[scope.cursor];
199
+ scope.cursor += 1;
200
+ return next as TResume;
201
+ }
202
+ throw new PauseSignal<TValue>(value);
203
+ }
204
+
205
+ /**
206
+ * Cross-realm safe type guard for `PauseSignal`.
207
+ *
208
+ * @stable
209
+ */
210
+ export function isPauseSignal(err: unknown): err is PauseSignal {
211
+ return (
212
+ typeof err === 'object' &&
213
+ err !== null &&
214
+ (err as Record<symbol, unknown>)[PAUSE_SIGNAL_BRAND] === true
215
+ );
216
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Persisted server auth token record. Holds a per-token HMAC hash + scope
3
+ * grammar; raw tokens are never persisted (the runtime carries them via
4
+ * `SecretValue`).
5
+ *
6
+ * @stable
7
+ */
8
+ export interface AuthTokenRecord {
9
+ /** Stable identifier (the public part of the token, before the secret). */
10
+ readonly id: string;
11
+ /** HMAC-SHA256 of the secret part, peppered. Hex-encoded. */
12
+ readonly hashHex: string;
13
+ /** Optional human-readable label rendered in CLI listings. */
14
+ readonly label?: string;
15
+ /** Scope grammar - opaque strings of the form `<resource>:<action>[:<id-or-glob>]`. */
16
+ readonly scopes: ReadonlyArray<string>;
17
+ readonly createdAt: string;
18
+ readonly expiresAt?: string;
19
+ readonly revokedAt?: string;
20
+ readonly lastUsedAt?: string;
21
+ }
22
+
23
+ /**
24
+ * Pluggable storage for server auth tokens. The default implementation
25
+ * lives in `@graphorin/store-sqlite` (`auth_tokens` table). The server
26
+ * package implements `verifyToken(...)` on top of this contract.
27
+ *
28
+ * @stable
29
+ */
30
+ export interface AuthTokenStore {
31
+ put(record: AuthTokenRecord): Promise<void>;
32
+ get(id: string): Promise<AuthTokenRecord | null>;
33
+ list(): Promise<ReadonlyArray<AuthTokenRecord>>;
34
+ revoke(id: string, revokedAt: string): Promise<void>;
35
+ recordUse(id: string, usedAt: string): Promise<void>;
36
+ /**
37
+ * Indexed lookup by HMAC hash (SPL-19). When present, the verifier
38
+ * uses it on cache-miss instead of walking `list()` - O(1) instead of
39
+ * an O(n) full-table scan per verification.
40
+ */
41
+ getByHash?(hashHex: string): Promise<AuthTokenRecord | null>;
42
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Opaque identifier for a single workflow checkpoint. Treated as a string
3
+ * by every consumer so adapters can pick whatever encoding they prefer
4
+ * (ULID, UUID, snowflake-like, …).
5
+ *
6
+ * @stable
7
+ */
8
+ export type CheckpointId = string;
9
+
10
+ /**
11
+ * Serialized snapshot of workflow state, written after every execution
12
+ * step.
13
+ *
14
+ * @stable
15
+ */
16
+ export interface Checkpoint {
17
+ readonly id: CheckpointId;
18
+ readonly threadId: string;
19
+ readonly namespace: string;
20
+ readonly parentId?: CheckpointId;
21
+ /** Serialized state blob - adapter-specific encoding (JSON / superjson / …). */
22
+ readonly state: unknown;
23
+ /** Per-channel monotonic versions used by the workflow scheduler. */
24
+ readonly channelVersions: Readonly<Record<string, number>>;
25
+ readonly stepNumber: number;
26
+ readonly createdAt: string;
27
+ }
28
+
29
+ /**
30
+ * Metadata associated with a checkpoint write. Adapters store this in a
31
+ * sidecar table for efficient listing.
32
+ *
33
+ * @stable
34
+ */
35
+ export interface CheckpointMetadata {
36
+ /**
37
+ * Durability mode that produced this write. The legacy `'async'`
38
+ * value was removed (workflow-14 / WF-7 - it was byte-identical to
39
+ * `'sync'`); adapters normalize legacy persisted rows to `'sync'` at
40
+ * read time.
41
+ */
42
+ readonly source: 'sync' | 'exit';
43
+ readonly status: 'running' | 'suspended' | 'completed' | 'failed' | 'aborted';
44
+ readonly nodeName?: string;
45
+ readonly tags?: ReadonlyArray<string>;
46
+ /**
47
+ * Session this checkpoint's state belongs to, when known (W-005).
48
+ * The agent runtime stamps it on every HITL-suspend write so a
49
+ * session hard-delete can cascade into `workflow_checkpoints` /
50
+ * `workflow_pending_writes` without parsing the opaque state blob.
51
+ * Optional and additive: third-party stores may ignore it, but any
52
+ * store that also implements `SessionStoreExt.deleteSession` should
53
+ * use it to honour the full erasure contract.
54
+ */
55
+ readonly sessionId?: string;
56
+ /**
57
+ * W-032: earliest due durable timer among this checkpoint's frontier
58
+ * pauses (epoch ms). The workflow engine stamps it on every
59
+ * `suspended` write whose frontier holds a timer, so a store's
60
+ * {@link CheckpointStore.listSuspended} can enumerate due threads
61
+ * without parsing the opaque tags. Absent on non-timer suspends and
62
+ * on checkpoints written before the field existed - such threads
63
+ * need one manual `tick` (or a resume) to become driver-visible.
64
+ */
65
+ readonly wakeAt?: number;
66
+ }
67
+
68
+ /**
69
+ * A checkpoint paired with its sidecar metadata. Returned by
70
+ * `CheckpointStore.getTuple(...)` and the `list(...)` iterator.
71
+ *
72
+ * @stable
73
+ */
74
+ export interface CheckpointTuple {
75
+ readonly checkpoint: Checkpoint;
76
+ readonly metadata: CheckpointMetadata;
77
+ readonly pendingWrites?: ReadonlyArray<PendingWrite>;
78
+ }
79
+
80
+ /**
81
+ * Per-task pending write. Captured when a task in an execution step
82
+ * succeeds while a sibling task fails: the next resume attempt skips the
83
+ * already-completed work.
84
+ *
85
+ * @stable
86
+ */
87
+ export interface PendingWrite {
88
+ readonly taskId: string;
89
+ readonly index: number;
90
+ readonly channel: string;
91
+ /** Serialized value blob - adapter-specific encoding. */
92
+ readonly value: unknown;
93
+ }
94
+
95
+ /**
96
+ * Optional listing range for `CheckpointStore.list(...)`.
97
+ *
98
+ * @stable
99
+ */
100
+ export interface ListOptions {
101
+ readonly limit?: number;
102
+ readonly before?: CheckpointId;
103
+ readonly status?: CheckpointMetadata['status'];
104
+ }
105
+
106
+ /**
107
+ * Optional atomicity contract for {@link CheckpointStore.put} (D1 /
108
+ * workflow-01). When `expectedLatestId` is supplied, the store MUST
109
+ * perform the latest-checkpoint comparison and the insert atomically
110
+ * (single transaction / synchronous critical section) and throw
111
+ * {@link CheckpointConflictError} on mismatch - closing the TOCTOU
112
+ * window an engine-level read-then-write cannot. `null` means "expect
113
+ * no checkpoint for this thread yet"; `undefined` (or a store that
114
+ * ignores the argument) preserves the unguarded legacy behaviour, which
115
+ * the engine backstops with its own pre-check.
116
+ *
117
+ * @stable
118
+ */
119
+ export interface CheckpointPutOptions {
120
+ readonly expectedLatestId?: CheckpointId | null;
121
+ }
122
+
123
+ /**
124
+ * Thrown by a {@link CheckpointStore.put} honouring
125
+ * {@link CheckpointPutOptions.expectedLatestId} when another writer
126
+ * advanced the thread in between. The workflow engine maps it to its
127
+ * `checkpoint-version-conflict` error.
128
+ *
129
+ * @stable
130
+ */
131
+ export class CheckpointConflictError extends Error {
132
+ readonly threadId: string;
133
+ readonly expectedLatestId: CheckpointId | null;
134
+ readonly actualLatestId: CheckpointId | null;
135
+
136
+ constructor(threadId: string, expected: CheckpointId | null, actual: CheckpointId | null) {
137
+ super(
138
+ `checkpoint conflict on thread "${threadId}": expected latest ${expected ?? '<none>'}, found ${actual ?? '<none>'}`,
139
+ );
140
+ this.name = 'CheckpointConflictError';
141
+ this.threadId = threadId;
142
+ this.expectedLatestId = expected;
143
+ this.actualLatestId = actual;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Pluggable checkpoint storage interface. The default implementation
149
+ * lives in `@graphorin/store-sqlite`.
150
+ *
151
+ * @stable
152
+ */
153
+ export interface CheckpointStore {
154
+ put(
155
+ threadId: string,
156
+ namespace: string,
157
+ checkpoint: Checkpoint,
158
+ metadata: CheckpointMetadata,
159
+ opts?: CheckpointPutOptions,
160
+ ): Promise<CheckpointId>;
161
+
162
+ putWrites(
163
+ threadId: string,
164
+ namespace: string,
165
+ checkpointId: CheckpointId,
166
+ writes: ReadonlyArray<PendingWrite>,
167
+ taskId: string,
168
+ ): Promise<void>;
169
+
170
+ getTuple(
171
+ threadId: string,
172
+ namespace: string,
173
+ checkpointId?: CheckpointId,
174
+ ): Promise<CheckpointTuple | null>;
175
+
176
+ list(threadId: string, namespace: string, opts?: ListOptions): AsyncIterable<CheckpointTuple>;
177
+
178
+ /**
179
+ * Full erasure primitive: delete every checkpoint and pending write of
180
+ * this thread across ALL namespaces. Namespace-blind by contract -
181
+ * retention sweeps must use {@link CheckpointStoreExt.pruneThreads}
182
+ * instead, which is namespace-scoped and protects suspended threads.
183
+ */
184
+ deleteThread(threadId: string): Promise<void>;
185
+
186
+ /**
187
+ * W-032: enumerate threads whose LATEST checkpoint in `namespace` is
188
+ * `suspended` with a due `wakeAt` (`<= opts.dueBefore`, default: any
189
+ * stamped wakeAt). This is what a durable-timer driver polls -
190
+ * without it an operator would have to keep an external registry of
191
+ * sleeping threadIds. OPTIONAL so third-party stores compile
192
+ * unchanged; `createTimerDriver` throws a typed error when the store
193
+ * lacks it (deterministic policy, no silent no-op).
194
+ */
195
+ listSuspended?(
196
+ namespace: string,
197
+ opts?: { readonly dueBefore?: number; readonly limit?: number },
198
+ ): Promise<ReadonlyArray<{ readonly threadId: string; readonly wakeAt: number }>>;
199
+ }
200
+
201
+ /**
202
+ * Options for {@link CheckpointStoreExt.pruneThreads}.
203
+ *
204
+ * @stable
205
+ */
206
+ export interface PruneThreadsOptions {
207
+ /**
208
+ * Cutoff: a `(threadId, namespace)` pair qualifies when its LATEST
209
+ * checkpoint (by `stepNumber`) was created before this epoch-ms
210
+ * instant.
211
+ */
212
+ readonly beforeEpochMs: number;
213
+ /**
214
+ * When `true` (the default), only pairs whose latest checkpoint has a
215
+ * terminal status (`completed` / `failed` / `aborted`) are pruned -
216
+ * suspended threads hold live HITL approvals / awakeables and must
217
+ * survive a retention sweep. Set to `false` for a hard age-based
218
+ * sweep that also removes suspended threads.
219
+ */
220
+ readonly onlyTerminal?: boolean;
221
+ }
222
+
223
+ /**
224
+ * Retention extension over {@link CheckpointStore} (W-009). The engine
225
+ * intentionally never deletes finished threads itself - a completed
226
+ * thread is still needed for inspection and duplicate-resume refusal;
227
+ * how long to keep it is an operator decision. These primitives are
228
+ * what an operator (or a host scheduler) drives.
229
+ *
230
+ * Additive: third-party `CheckpointStore` implementations compile
231
+ * unchanged; hosts feature-detect with `'pruneThreads' in store`.
232
+ *
233
+ * @stable
234
+ */
235
+ export interface CheckpointStoreExt extends CheckpointStore {
236
+ /**
237
+ * Namespace-scoped retention sweep: for every `(threadId, namespace)`
238
+ * pair matching the policy, delete that pair's checkpoints and pending
239
+ * writes - and ONLY that pair's. Never implemented via
240
+ * {@link CheckpointStore.deleteThread} (namespace-blind): with a
241
+ * reused threadId, pruning a terminal thread of workflow A must not
242
+ * erase the suspended checkpoints of workflow B. Returns the number
243
+ * of pairs pruned.
244
+ */
245
+ pruneThreads(opts: PruneThreadsOptions): Promise<number>;
246
+
247
+ /**
248
+ * Keep only the `keepLast` most recent checkpoints (by `stepNumber`)
249
+ * of one `(threadId, namespace)` pair, deleting older ones together
250
+ * with their pending writes. `keepLast >= 1`; resume works from the
251
+ * latest tuple, so compaction never breaks resumability - it does
252
+ * remove time-travel/fork targets. Returns the number of checkpoints
253
+ * deleted.
254
+ */
255
+ compactThread(threadId: string, namespace: string, keepLast: number): Promise<number>;
256
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Pluggable embedding provider. Implementations live in the embedder
3
+ * adapter packages (`@graphorin/embedder-transformersjs` (default),
4
+ * `@graphorin/embedder-ollama`, …).
5
+ *
6
+ * Each embedder advertises its model `id`, output `dim`, and a stable
7
+ * `configHash` used by the multi-table per-embedder vec0 layout in the
8
+ * default SQLite store: facts indexed under embedder A and facts indexed
9
+ * under embedder B live in separate vec0 tables; the `configHash` is the
10
+ * lookup key.
11
+ *
12
+ * @stable
13
+ */
14
+ export interface EmbedderProvider {
15
+ /** Stable identifier (e.g. `'transformersjs:Xenova/multilingual-e5-base'`). */
16
+ id(): string;
17
+ /** Output dimensionality of the embedding vectors. */
18
+ dim(): number;
19
+ /** Stable hash of the embedder's configuration (model + revision + opts). */
20
+ configHash(): string;
21
+ /** Compute embeddings for a batch of texts. Returns one vector per text. */
22
+ embed(texts: ReadonlyArray<string>, opts?: EmbedOptions): Promise<ReadonlyArray<Float32Array>>;
23
+ }
24
+
25
+ /**
26
+ * Per-call options for `EmbedderProvider.embed(...)`.
27
+ *
28
+ * @stable
29
+ */
30
+ export interface EmbedOptions {
31
+ readonly signal?: AbortSignal;
32
+ /** Optional per-call request id forwarded to the trace span. */
33
+ readonly requestId?: string;
34
+ /**
35
+ * Asymmetric retrieval role of the input (PS-10). Embedders for models that
36
+ * require asymmetric prefixes - the E5 family's `query:` / `passage:` - apply
37
+ * the matching prefix; embedders for symmetric models ignore it. Memory tiers
38
+ * pass `'query'` when embedding a search query and `'passage'` when embedding
39
+ * content for storage.
40
+ */
41
+ readonly taskType?: 'query' | 'passage';
42
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Pluggable evaluation scorer. Generic over the input / output types
3
+ * carried by the eval dataset. Concrete implementations live in the
4
+ * separate `@graphorin/evals` package (post-MVP); the interface lives
5
+ * here so that:
6
+ *
7
+ * - The minimal inline runner shipped with `@graphorin/observability` can
8
+ * type-check against it.
9
+ * - Custom application-level scorers can be defined without taking an
10
+ * evals dependency.
11
+ *
12
+ * @stable
13
+ */
14
+ export interface EvalScorer<TInput = unknown, TOutput = unknown> {
15
+ readonly id: string;
16
+ readonly description?: string;
17
+ score(sample: EvalSample<TInput, TOutput>): Promise<EvalScore>;
18
+ }
19
+
20
+ /**
21
+ * A single sample from an eval dataset.
22
+ *
23
+ * @stable
24
+ */
25
+ export interface EvalSample<TInput = unknown, TOutput = unknown> {
26
+ readonly id: string;
27
+ readonly input: TInput;
28
+ readonly expected?: TOutput;
29
+ readonly actual: TOutput;
30
+ readonly metadata?: Readonly<Record<string, unknown>>;
31
+ }
32
+
33
+ /**
34
+ * Result of `EvalScorer.score(...)`. `value` is normalized to `[0, 1]`
35
+ * by convention; raw scores can be carried in `details`.
36
+ *
37
+ * @stable
38
+ */
39
+ export interface EvalScore {
40
+ readonly value: number;
41
+ readonly pass?: boolean;
42
+ readonly rationale?: string;
43
+ readonly details?: Readonly<Record<string, unknown>>;
44
+ }