@graphorin/core 0.6.1 → 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.
- package/CHANGELOG.md +82 -0
- package/README.md +8 -5
- package/dist/channels/index.d.ts +2 -2
- package/dist/channels/index.js +2 -2
- package/dist/channels/pause.d.ts +47 -2
- package/dist/channels/pause.d.ts.map +1 -1
- package/dist/channels/pause.js +62 -2
- package/dist/channels/pause.js.map +1 -1
- package/dist/contracts/checkpoint-store.d.ts +97 -1
- package/dist/contracts/checkpoint-store.d.ts.map +1 -1
- package/dist/contracts/checkpoint-store.js.map +1 -1
- package/dist/contracts/index.d.ts +5 -5
- package/dist/contracts/index.js +2 -1
- package/dist/contracts/memory-store.d.ts +59 -2
- package/dist/contracts/memory-store.d.ts.map +1 -1
- package/dist/contracts/provider.d.ts +20 -6
- package/dist/contracts/provider.d.ts.map +1 -1
- package/dist/contracts/session-store.d.ts +10 -2
- package/dist/contracts/session-store.d.ts.map +1 -1
- package/dist/contracts/tool.d.ts +75 -1
- package/dist/contracts/tool.d.ts.map +1 -1
- package/dist/contracts/tool.js +57 -0
- package/dist/contracts/tool.js.map +1 -0
- package/dist/contracts/tracer.d.ts +53 -5
- package/dist/contracts/tracer.d.ts.map +1 -1
- package/dist/contracts/tracer.js.map +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/dist/types/agent-event-wire.d.ts +74 -0
- package/dist/types/agent-event-wire.d.ts.map +1 -0
- package/dist/types/agent-event-wire.js +130 -0
- package/dist/types/agent-event-wire.js.map +1 -0
- package/dist/types/agent-event.d.ts +48 -6
- package/dist/types/agent-event.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -2
- package/dist/types/index.js +2 -1
- package/dist/types/memory.d.ts +11 -0
- package/dist/types/memory.d.ts.map +1 -1
- package/dist/types/run.d.ts +86 -4
- package/dist/types/run.d.ts.map +1 -1
- package/dist/types/run.js.map +1 -1
- package/dist/types/tool.d.ts +10 -0
- package/dist/types/tool.d.ts.map +1 -1
- package/dist/types/usage.d.ts +11 -1
- package/dist/types/usage.d.ts.map +1 -1
- package/dist/types/usage.js.map +1 -1
- package/dist/utils/binary-json.d.ts +165 -0
- package/dist/utils/binary-json.d.ts.map +1 -0
- package/dist/utils/binary-json.js +240 -0
- package/dist/utils/binary-json.js.map +1 -0
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.js +2 -1
- package/dist/utils/validation.d.ts +10 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +1 -1
- package/dist/utils/validation.js.map +1 -1
- package/package.json +9 -7
- package/src/channels/channels.ts +206 -0
- package/src/channels/directive.ts +41 -0
- package/src/channels/dispatch.ts +37 -0
- package/src/channels/durable.ts +151 -0
- package/src/channels/index.ts +62 -0
- package/src/channels/pause.ts +216 -0
- package/src/contracts/auth-token-store.ts +42 -0
- package/src/contracts/checkpoint-store.ts +256 -0
- package/src/contracts/embedder.ts +42 -0
- package/src/contracts/eval-scorer.ts +44 -0
- package/src/contracts/index.ts +112 -0
- package/src/contracts/local-provider-trust.ts +33 -0
- package/src/contracts/logger.ts +61 -0
- package/src/contracts/memory-store.ts +187 -0
- package/src/contracts/oauth-server-store.ts +78 -0
- package/src/contracts/preferred-model.ts +56 -0
- package/src/contracts/provider.ts +316 -0
- package/src/contracts/reasoning-retention.ts +52 -0
- package/src/contracts/redaction-validator.ts +56 -0
- package/src/contracts/sandbox.ts +70 -0
- package/src/contracts/secret-ref.ts +22 -0
- package/src/contracts/secret-value.ts +117 -0
- package/src/contracts/secrets-store.ts +90 -0
- package/src/contracts/session-store.ts +163 -0
- package/src/contracts/token-counter.ts +23 -0
- package/src/contracts/tool.ts +397 -0
- package/src/contracts/tracer.ts +219 -0
- package/src/contracts/trigger-store.ts +40 -0
- package/src/index.ts +23 -0
- package/src/types/agent-event-wire.ts +193 -0
- package/src/types/agent-event.ts +579 -0
- package/src/types/handoff.ts +111 -0
- package/src/types/index.ts +148 -0
- package/src/types/memory.ts +427 -0
- package/src/types/message.ts +174 -0
- package/src/types/run.ts +312 -0
- package/src/types/sensitivity.ts +35 -0
- package/src/types/session-scope.ts +18 -0
- package/src/types/stop-condition.ts +108 -0
- package/src/types/tool-call.ts +24 -0
- package/src/types/tool.ts +324 -0
- package/src/types/usage.ts +120 -0
- package/src/types/workflow-event.ts +132 -0
- package/src/utils/assert-never.ts +24 -0
- package/src/utils/async-context.ts +55 -0
- package/src/utils/binary-json.ts +425 -0
- package/src/utils/hash.ts +122 -0
- package/src/utils/index.ts +57 -0
- package/src/utils/streams.ts +233 -0
- 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
|
+
}
|