melchizedek-agents 1.6.0 → 1.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 +83 -0
- package/DOCUMENTATION.md +5 -1
- package/config/agents/syndicate.schema.json +63 -0
- package/config/agents/syndicateSchema.yaml +50 -0
- package/dist/lib/a2a/executor.js +21 -0
- package/dist/lib/compile.d.ts +4 -3
- package/dist/lib/compile.js +8 -3
- package/dist/lib/index.d.ts +8 -0
- package/dist/lib/index.js +5 -0
- package/dist/lib/loadSyndicate.d.ts +13 -0
- package/dist/lib/loadSyndicate.js +11 -1
- package/dist/lib/observability/supabaseSpanExporter.js +2 -1
- package/dist/lib/observability/tracer.d.ts +2 -2
- package/dist/lib/observability/tracer.js +2 -2
- package/dist/lib/runtime/native/checkpoint.d.ts +2 -1
- package/dist/lib/runtime/native/checkpoint.js +6 -2
- package/dist/lib/runtime/native/interrupts.js +9 -2
- package/dist/lib/runtime/nativeTurn.d.ts +2 -1
- package/dist/lib/runtime/nativeTurn.js +5 -1
- package/dist/lib/runtime/syndicateTurn.d.ts +11 -1
- package/dist/lib/runtime/syndicateTurn.js +383 -5
- package/dist/lib/sandbox/sandbox.d.ts +121 -0
- package/dist/lib/sandbox/sandbox.js +276 -0
- package/dist/lib/sandbox/worker.d.ts +29 -0
- package/dist/lib/sandbox/worker.js +280 -0
- package/dist/lib/script/config.d.ts +35 -0
- package/dist/lib/script/config.js +17 -0
- package/dist/lib/script/journal.d.ts +137 -0
- package/dist/lib/script/journal.js +193 -0
- package/dist/lib/script/run.d.ts +138 -0
- package/dist/lib/script/run.js +364 -0
- package/dist/lib/syndicateSchema.d.ts +20 -0
- package/dist/lib/syndicateSchema.js +98 -6
- package/dist/lib/toolRegistry.js +4 -0
- package/dist/lib/tools/runCodeTool.d.ts +50 -0
- package/dist/lib/tools/runCodeTool.js +59 -0
- package/package.json +5 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/script/journal.ts — a script run's record of its calls, kept as
|
|
3
|
+
* node-style events in the conversation (ADR 0137 items 8, 9 and 11).
|
|
4
|
+
*
|
|
5
|
+
* ONE EVENT PER CALL. Each agent, tool, askUser, now and random call a run
|
|
6
|
+
* makes is one event: authored by the script's name, `nodeInfo.path`
|
|
7
|
+
* `<script>.<seq>`, the call in `actions.agentState.script_call` (the run,
|
|
8
|
+
* its seq, the kind, the name and a hash of the arguments) and its result in
|
|
9
|
+
* `event.output`, with no content parts. Those are the fields the Supabase
|
|
10
|
+
* store's trimEventForStorage never touches, so the journal replays the same
|
|
11
|
+
* from it and from Postgres; and with no text part, long-term memory
|
|
12
|
+
* ingestion (which reads text) never sees it.
|
|
13
|
+
*
|
|
14
|
+
* THE PAUSE. A live askUser writes the request ADK's ask_user node writes
|
|
15
|
+
* (`adk_request_input`, lib/workflow/pause.ts), so `result.input` is one
|
|
16
|
+
* shape for every method, plus `script_pause`: the run's pinned source, its
|
|
17
|
+
* message and transcript, which the resume runs again (item 11). Its answer
|
|
18
|
+
* is a second event for the same seq, carrying `{ reply, input }`.
|
|
19
|
+
*
|
|
20
|
+
* WHEN IT IS STORED. Only a run that pauses needs its journal, to replay
|
|
21
|
+
* it, so the runner keeps the events in memory and stores them with the
|
|
22
|
+
* pause; a run that finishes stores none, and the conversation keeps only
|
|
23
|
+
* the exchange, as for a dispatch route (on the Supabase store every append
|
|
24
|
+
* rewrites the session's events). A resumed run that ends stores its
|
|
25
|
+
* answer's event and an `end` marker, closing it.
|
|
26
|
+
*
|
|
27
|
+
* WHICH RUN IS OPEN. The conversation's last journal event decides: a pause
|
|
28
|
+
* with no answer after it is a run waiting for the person; anything else is
|
|
29
|
+
* a finished run, and the next message starts a new one.
|
|
30
|
+
*/
|
|
31
|
+
import type { TurnEvent } from '../runtime/events.ts';
|
|
32
|
+
import type { PendingApproval } from '../runtime/approvals.ts';
|
|
33
|
+
import type { PendingInput } from '../workflowConfig.ts';
|
|
34
|
+
/** What a script calls. */
|
|
35
|
+
/** What a script calls; `end` marks a resumed run that finished, so the next message starts a new one. */
|
|
36
|
+
export type ScriptCallKind = 'agent' | 'tool' | 'askUser' | 'now' | 'random' | 'end';
|
|
37
|
+
/** `actions.agentState.script_call`. */
|
|
38
|
+
export interface ScriptCallRecord {
|
|
39
|
+
run: string;
|
|
40
|
+
seq: number;
|
|
41
|
+
kind: ScriptCallKind;
|
|
42
|
+
name: string;
|
|
43
|
+
hash: string;
|
|
44
|
+
}
|
|
45
|
+
/** `actions.agentState.script_run`: what a resume runs again, pinned when the run first stores anything. */
|
|
46
|
+
export interface ScriptPinRecord {
|
|
47
|
+
run: string;
|
|
48
|
+
source: string;
|
|
49
|
+
source_hash: string;
|
|
50
|
+
message: string;
|
|
51
|
+
transcript: string;
|
|
52
|
+
}
|
|
53
|
+
/** `actions.agentState.script_pause`: the pin, and what the run waits on. */
|
|
54
|
+
export interface ScriptPauseRecord extends ScriptPinRecord {
|
|
55
|
+
/** The interrupt id the person's answer resolves: a question's, or an approval request's. */
|
|
56
|
+
interrupt_id: string;
|
|
57
|
+
/** `question` (askUser) or `approval` (a gated tool() call). Absent: question. */
|
|
58
|
+
kind?: 'question' | 'approval';
|
|
59
|
+
}
|
|
60
|
+
/** What one call returned, as the journal keeps it: a value, or the error it threw into the script. */
|
|
61
|
+
export type ScriptCallOutcome = {
|
|
62
|
+
value: unknown;
|
|
63
|
+
} | {
|
|
64
|
+
error: {
|
|
65
|
+
name: string;
|
|
66
|
+
message: string;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
/** One recorded call. */
|
|
70
|
+
export interface JournalEntry extends ScriptCallRecord {
|
|
71
|
+
/** Absent for a pause still waiting for its answer. */
|
|
72
|
+
outcome?: ScriptCallOutcome;
|
|
73
|
+
/** The pause record, on an askUser that paused the run. */
|
|
74
|
+
pause?: ScriptPauseRecord;
|
|
75
|
+
}
|
|
76
|
+
/** The journal events' marker keys under agentState. */
|
|
77
|
+
export declare const SCRIPT_CALL = "script_call";
|
|
78
|
+
export declare const SCRIPT_PAUSE = "script_pause";
|
|
79
|
+
export declare const SCRIPT_RUN = "script_run";
|
|
80
|
+
/**
|
|
81
|
+
* Set on a session store that wants every journal event as its call ends,
|
|
82
|
+
* not only when the run pauses: the durable-run store (ADR 0113), whose
|
|
83
|
+
* checkpoint boundary a journal append is, so a worker killed mid-script
|
|
84
|
+
* resumes from its last call (ADR 0137 item 8).
|
|
85
|
+
*/
|
|
86
|
+
export declare const JOURNAL_EVERY_CALL: unique symbol;
|
|
87
|
+
/** True when `sessions` asks for every journal event as its call ends. */
|
|
88
|
+
export declare function journalsEveryCall(sessions: unknown): boolean;
|
|
89
|
+
/** True for a script's journal event. */
|
|
90
|
+
export declare function isJournalEvent(event: TurnEvent): boolean;
|
|
91
|
+
/** The hash a call is matched on: its kind, name and arguments. */
|
|
92
|
+
export declare function callHash(kind: ScriptCallKind, name: string, args: unknown): string;
|
|
93
|
+
/** The hash a run's source is pinned by. */
|
|
94
|
+
export declare function sourceHash(source: string): string;
|
|
95
|
+
/** The event for a call that returned (or threw into the script); `pin` on the first event a run stores call by call. */
|
|
96
|
+
export declare function callEvent(script: string, invocationId: string, call: ScriptCallRecord, outcome: ScriptCallOutcome, pin?: ScriptPinRecord): TurnEvent;
|
|
97
|
+
/** The marker a resumed run that ended leaves: its run is closed. */
|
|
98
|
+
export declare function endEvent(script: string, invocationId: string, run: string, seq: number): TurnEvent;
|
|
99
|
+
/** The event for an askUser that pauses the run: ADK's input request, stamped as a journal entry, with the pinned run. */
|
|
100
|
+
export declare function pauseEvent(script: string, invocationId: string, call: ScriptCallRecord, pause: ScriptPauseRecord, request: {
|
|
101
|
+
message: string;
|
|
102
|
+
payload?: unknown;
|
|
103
|
+
schema?: Record<string, unknown>;
|
|
104
|
+
}): TurnEvent;
|
|
105
|
+
/**
|
|
106
|
+
* The event for a gated tool() call that pauses the run: ADK's approval
|
|
107
|
+
* request (`adk_request_confirmation`, lib/runtime/approvals.ts), pinning
|
|
108
|
+
* the call and its arguments, stamped as a journal entry, with the pinned run.
|
|
109
|
+
*/
|
|
110
|
+
export declare function approvalEvent(script: string, invocationId: string, call: ScriptCallRecord, pause: ScriptPauseRecord, request: {
|
|
111
|
+
tool: string;
|
|
112
|
+
args: unknown;
|
|
113
|
+
hint: string;
|
|
114
|
+
}): TurnEvent;
|
|
115
|
+
/** A run the conversation has not finished: one waiting on the person, or one a stopped worker left mid-way. */
|
|
116
|
+
export interface OpenRun {
|
|
117
|
+
/** `paused`: its last event is a pause. `unfinished`: a run stored call by call whose last event is a call (a durable job resumed after its worker stopped). */
|
|
118
|
+
state: 'paused' | 'unfinished';
|
|
119
|
+
pin: ScriptPinRecord;
|
|
120
|
+
/** The pause, when paused. */
|
|
121
|
+
pause?: ScriptPauseRecord;
|
|
122
|
+
/** Every call it recorded, in seq order. */
|
|
123
|
+
entries: JournalEntry[];
|
|
124
|
+
}
|
|
125
|
+
/** The run a conversation has open, or undefined: the last journal event decides (a pause, a call of a run stored call by call, or a closing `end`). */
|
|
126
|
+
export declare function openRun(events: readonly TurnEvent[], script: string): OpenRun | undefined;
|
|
127
|
+
/**
|
|
128
|
+
* What a script run in a conversation waits on, read as a caller reads a
|
|
129
|
+
* nested syndicate's conversation (lib/runtime/native/interrupts.ts): the
|
|
130
|
+
* open pause of the run whose last event it is, as an approval request or a
|
|
131
|
+
* question, authored by the script. With `id`, only that request counts.
|
|
132
|
+
*/
|
|
133
|
+
export declare function scriptPauseIn(events: readonly TurnEvent[], id?: string): {
|
|
134
|
+
asker: string;
|
|
135
|
+
approval?: PendingApproval;
|
|
136
|
+
question?: PendingInput;
|
|
137
|
+
} | undefined;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/script/journal.ts — a script run's record of its calls, kept as
|
|
3
|
+
* node-style events in the conversation (ADR 0137 items 8, 9 and 11).
|
|
4
|
+
*
|
|
5
|
+
* ONE EVENT PER CALL. Each agent, tool, askUser, now and random call a run
|
|
6
|
+
* makes is one event: authored by the script's name, `nodeInfo.path`
|
|
7
|
+
* `<script>.<seq>`, the call in `actions.agentState.script_call` (the run,
|
|
8
|
+
* its seq, the kind, the name and a hash of the arguments) and its result in
|
|
9
|
+
* `event.output`, with no content parts. Those are the fields the Supabase
|
|
10
|
+
* store's trimEventForStorage never touches, so the journal replays the same
|
|
11
|
+
* from it and from Postgres; and with no text part, long-term memory
|
|
12
|
+
* ingestion (which reads text) never sees it.
|
|
13
|
+
*
|
|
14
|
+
* THE PAUSE. A live askUser writes the request ADK's ask_user node writes
|
|
15
|
+
* (`adk_request_input`, lib/workflow/pause.ts), so `result.input` is one
|
|
16
|
+
* shape for every method, plus `script_pause`: the run's pinned source, its
|
|
17
|
+
* message and transcript, which the resume runs again (item 11). Its answer
|
|
18
|
+
* is a second event for the same seq, carrying `{ reply, input }`.
|
|
19
|
+
*
|
|
20
|
+
* WHEN IT IS STORED. Only a run that pauses needs its journal, to replay
|
|
21
|
+
* it, so the runner keeps the events in memory and stores them with the
|
|
22
|
+
* pause; a run that finishes stores none, and the conversation keeps only
|
|
23
|
+
* the exchange, as for a dispatch route (on the Supabase store every append
|
|
24
|
+
* rewrites the session's events). A resumed run that ends stores its
|
|
25
|
+
* answer's event and an `end` marker, closing it.
|
|
26
|
+
*
|
|
27
|
+
* WHICH RUN IS OPEN. The conversation's last journal event decides: a pause
|
|
28
|
+
* with no answer after it is a run waiting for the person; anything else is
|
|
29
|
+
* a finished run, and the next message starts a new one.
|
|
30
|
+
*/
|
|
31
|
+
import { createHash } from 'node:crypto';
|
|
32
|
+
import { createTurnEvent } from '../runtime/events.js';
|
|
33
|
+
import { requestInputEvent } from '../workflow/pause.js';
|
|
34
|
+
import { APPROVAL_REQUEST, approvalRequestOf } from '../runtime/approvals.js';
|
|
35
|
+
import { getFunctionCalls } from '../runtime/events.js';
|
|
36
|
+
import { inputRequestFrom } from '../workflowConfig.js';
|
|
37
|
+
/** The journal events' marker keys under agentState. */
|
|
38
|
+
export const SCRIPT_CALL = 'script_call';
|
|
39
|
+
export const SCRIPT_PAUSE = 'script_pause';
|
|
40
|
+
export const SCRIPT_RUN = 'script_run';
|
|
41
|
+
/**
|
|
42
|
+
* Set on a session store that wants every journal event as its call ends,
|
|
43
|
+
* not only when the run pauses: the durable-run store (ADR 0113), whose
|
|
44
|
+
* checkpoint boundary a journal append is, so a worker killed mid-script
|
|
45
|
+
* resumes from its last call (ADR 0137 item 8).
|
|
46
|
+
*/
|
|
47
|
+
export const JOURNAL_EVERY_CALL = Symbol.for('melchizedek.script.journalEveryCall');
|
|
48
|
+
/** True when `sessions` asks for every journal event as its call ends. */
|
|
49
|
+
export function journalsEveryCall(sessions) {
|
|
50
|
+
return !!sessions && typeof sessions === 'object' && sessions[JOURNAL_EVERY_CALL] === true;
|
|
51
|
+
}
|
|
52
|
+
/** True for a script's journal event. */
|
|
53
|
+
export function isJournalEvent(event) {
|
|
54
|
+
return !!recordOf(event);
|
|
55
|
+
}
|
|
56
|
+
/** A canonical JSON text: object keys sorted, so a hash does not depend on key order. */
|
|
57
|
+
function canonical(value) {
|
|
58
|
+
if (value === null || typeof value !== 'object')
|
|
59
|
+
return JSON.stringify(value) ?? 'null';
|
|
60
|
+
if (Array.isArray(value))
|
|
61
|
+
return `[${value.map(canonical).join(',')}]`;
|
|
62
|
+
const keys = Object.keys(value).filter((k) => value[k] !== undefined).sort();
|
|
63
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonical(value[k])}`).join(',')}}`;
|
|
64
|
+
}
|
|
65
|
+
/** The hash a call is matched on: its kind, name and arguments. */
|
|
66
|
+
export function callHash(kind, name, args) {
|
|
67
|
+
return createHash('sha256').update(canonical([kind, name, args])).digest('hex').slice(0, 24);
|
|
68
|
+
}
|
|
69
|
+
/** The hash a run's source is pinned by. */
|
|
70
|
+
export function sourceHash(source) {
|
|
71
|
+
return createHash('sha256').update(source).digest('hex').slice(0, 24);
|
|
72
|
+
}
|
|
73
|
+
/** The event for a call that returned (or threw into the script); `pin` on the first event a run stores call by call. */
|
|
74
|
+
export function callEvent(script, invocationId, call, outcome, pin) {
|
|
75
|
+
return createTurnEvent({
|
|
76
|
+
author: script,
|
|
77
|
+
invocationId,
|
|
78
|
+
nodeInfo: { path: `${script}.${call.seq}` },
|
|
79
|
+
actions: { agentState: { [SCRIPT_CALL]: { ...call }, ...(pin ? { [SCRIPT_RUN]: { ...pin } } : {}) } },
|
|
80
|
+
output: outcome,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** The marker a resumed run that ended leaves: its run is closed. */
|
|
84
|
+
export function endEvent(script, invocationId, run, seq) {
|
|
85
|
+
return createTurnEvent({
|
|
86
|
+
author: script,
|
|
87
|
+
invocationId,
|
|
88
|
+
nodeInfo: { path: `${script}.${seq}` },
|
|
89
|
+
actions: { agentState: { [SCRIPT_CALL]: { run, seq, kind: 'end', name: 'end', hash: '' } } },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/** The event for an askUser that pauses the run: ADK's input request, stamped as a journal entry, with the pinned run. */
|
|
93
|
+
export function pauseEvent(script, invocationId, call, pause, request) {
|
|
94
|
+
const event = requestInputEvent({ interruptId: pause.interrupt_id, message: request.message, payload: request.payload, ...(request.schema ? { responseSchema: request.schema } : {}) });
|
|
95
|
+
event.author = script;
|
|
96
|
+
event.invocationId = invocationId;
|
|
97
|
+
event.nodeInfo = { path: `${script}.${call.seq}` };
|
|
98
|
+
event.actions.agentState = { [SCRIPT_CALL]: { ...call }, [SCRIPT_PAUSE]: { ...pause } };
|
|
99
|
+
return event;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The event for a gated tool() call that pauses the run: ADK's approval
|
|
103
|
+
* request (`adk_request_confirmation`, lib/runtime/approvals.ts), pinning
|
|
104
|
+
* the call and its arguments, stamped as a journal entry, with the pinned run.
|
|
105
|
+
*/
|
|
106
|
+
export function approvalEvent(script, invocationId, call, pause, request) {
|
|
107
|
+
const originalFunctionCall = { name: request.tool, args: request.args ?? {}, id: `${call.run}.${call.seq}` };
|
|
108
|
+
return createTurnEvent({
|
|
109
|
+
author: script,
|
|
110
|
+
invocationId,
|
|
111
|
+
nodeInfo: { path: `${script}.${call.seq}` },
|
|
112
|
+
content: {
|
|
113
|
+
role: 'model',
|
|
114
|
+
parts: [{ functionCall: { name: APPROVAL_REQUEST, id: pause.interrupt_id, args: { originalFunctionCall, toolConfirmation: { hint: request.hint, confirmed: false } } } }],
|
|
115
|
+
},
|
|
116
|
+
actions: { agentState: { [SCRIPT_CALL]: { ...call }, [SCRIPT_PAUSE]: { ...pause, kind: 'approval' } } },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function recordOf(event) {
|
|
120
|
+
const rec = event.actions?.agentState?.[SCRIPT_CALL];
|
|
121
|
+
return rec && typeof rec.run === 'string' && typeof rec.seq === 'number' && typeof rec.kind === 'string' ? rec : undefined;
|
|
122
|
+
}
|
|
123
|
+
/** The run a conversation has open, or undefined: the last journal event decides (a pause, a call of a run stored call by call, or a closing `end`). */
|
|
124
|
+
export function openRun(events, script) {
|
|
125
|
+
let last;
|
|
126
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
127
|
+
const e = events[i];
|
|
128
|
+
if (e.author === script && recordOf(e)) {
|
|
129
|
+
last = e;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!last)
|
|
134
|
+
return undefined;
|
|
135
|
+
const lastRec = recordOf(last);
|
|
136
|
+
if (lastRec.kind === 'end')
|
|
137
|
+
return undefined;
|
|
138
|
+
const pause = last.actions.agentState?.[SCRIPT_PAUSE];
|
|
139
|
+
const run = lastRec.run;
|
|
140
|
+
const bySeq = new Map();
|
|
141
|
+
let pin = pause && typeof pause.source === 'string' ? pause : undefined;
|
|
142
|
+
for (const e of events) {
|
|
143
|
+
const rec = e.author === script ? recordOf(e) : undefined;
|
|
144
|
+
if (!rec || rec.run !== run)
|
|
145
|
+
continue;
|
|
146
|
+
const outcome = e.output;
|
|
147
|
+
const p = e.actions.agentState?.[SCRIPT_PAUSE];
|
|
148
|
+
const pinned = e.actions.agentState?.[SCRIPT_RUN];
|
|
149
|
+
if (!pin && pinned && typeof pinned.source === 'string')
|
|
150
|
+
pin = pinned;
|
|
151
|
+
const prior = bySeq.get(rec.seq);
|
|
152
|
+
bySeq.set(rec.seq, {
|
|
153
|
+
...rec,
|
|
154
|
+
...(outcome && typeof outcome === 'object' && ('value' in outcome || 'error' in outcome) ? { outcome } : prior?.outcome ? { outcome: prior.outcome } : {}),
|
|
155
|
+
...(p ? { pause: p } : prior?.pause ? { pause: prior.pause } : {}),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (!pin)
|
|
159
|
+
return undefined;
|
|
160
|
+
const entries = [...bySeq.values()].sort((a, b) => a.seq - b.seq);
|
|
161
|
+
// The last event a pause whose answer has not been stored: the run waits on it.
|
|
162
|
+
if (pause && typeof pause.source === 'string' && !bySeq.get(lastRec.seq)?.outcome)
|
|
163
|
+
return { state: 'paused', pin, pause, entries };
|
|
164
|
+
return { state: 'unfinished', pin, entries };
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* What a script run in a conversation waits on, read as a caller reads a
|
|
168
|
+
* nested syndicate's conversation (lib/runtime/native/interrupts.ts): the
|
|
169
|
+
* open pause of the run whose last event it is, as an approval request or a
|
|
170
|
+
* question, authored by the script. With `id`, only that request counts.
|
|
171
|
+
*/
|
|
172
|
+
export function scriptPauseIn(events, id) {
|
|
173
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
174
|
+
const e = events[i];
|
|
175
|
+
const rec = recordOf(e);
|
|
176
|
+
if (!rec || !e.author)
|
|
177
|
+
continue;
|
|
178
|
+
const pause = e.actions.agentState?.[SCRIPT_PAUSE];
|
|
179
|
+
if (!pause || e.output !== undefined || (id !== undefined && pause.interrupt_id !== id))
|
|
180
|
+
return undefined;
|
|
181
|
+
for (const call of getFunctionCalls(e)) {
|
|
182
|
+
if (call.id !== pause.interrupt_id)
|
|
183
|
+
continue;
|
|
184
|
+
if (call.name === APPROVAL_REQUEST)
|
|
185
|
+
return { asker: e.author, approval: approvalRequestOf(e, call) };
|
|
186
|
+
const question = inputRequestFrom(e.author, call);
|
|
187
|
+
if (question)
|
|
188
|
+
return { asker: e.author, question };
|
|
189
|
+
}
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/script/run.ts — one run of a syndicate's `script:` (ADR 0137 Part B):
|
|
3
|
+
* the script in the sandbox, its calls journaled, a paused or interrupted
|
|
4
|
+
* run resumed by replaying them.
|
|
5
|
+
*
|
|
6
|
+
* WHAT THE SCRIPT IS PASSED (item 6): `message`, `transcript` and
|
|
7
|
+
* `variables`, and the functions `agent`, `tool`, `askUser`, `now`,
|
|
8
|
+
* `random`, `log`, plus the sandbox's `parallel`. Nothing else reaches it.
|
|
9
|
+
* The host (lib/runtime/syndicateTurn.ts) supplies what `agent` and `tool`
|
|
10
|
+
* do; this module owns the journal, the replay, the pauses and the spans.
|
|
11
|
+
*
|
|
12
|
+
* REPLAY (item 9). A paused run's next message runs the script again from
|
|
13
|
+
* the top, on the source, message and transcript the run pinned. Each call
|
|
14
|
+
* is answered from the journal while one is recorded for it: a call is
|
|
15
|
+
* matched on its kind, its name and the hash of its arguments, the first
|
|
16
|
+
* unconsumed entry that matches, so concurrency under `parallel` (whose
|
|
17
|
+
* calls start in an order that depends on what finished first) replays the
|
|
18
|
+
* same. The open pause takes the person's answer; the calls after it run
|
|
19
|
+
* live. A run that ends with recorded calls it never made again diverged:
|
|
20
|
+
* it fails SCRIPT_DIVERGED, naming the first. Replayed calls are never
|
|
21
|
+
* charged again, open no span, and log() lines are not repeated while the
|
|
22
|
+
* run replays.
|
|
23
|
+
*
|
|
24
|
+
* THE PAUSES (items 8 and 10). A live askUser, and a tool() call to a tool
|
|
25
|
+
* in `script.require_approval`, let the calls already in flight settle,
|
|
26
|
+
* store the run's journal and the pause, then halt the sandbox; a call
|
|
27
|
+
* started after it waits forever and dies with the run, so a second pause
|
|
28
|
+
* is reached only once the first is answered. A question's answer is the
|
|
29
|
+
* next message (a JSON reply to a question with a schema is parsed, plain
|
|
30
|
+
* text passes as is, as an ask_user node's). An approval's answer is the
|
|
31
|
+
* decision: approved, the pinned arguments run; refused, the script gets
|
|
32
|
+
* `{ error }`, as a refused tool node's output is.
|
|
33
|
+
*
|
|
34
|
+
* WHEN THE JOURNAL IS STORED. Only a paused run reads it, so by default it
|
|
35
|
+
* is stored with the pause and a run that finishes stores none (on the
|
|
36
|
+
* Supabase store every append rewrites the session's events). A durable
|
|
37
|
+
* run's store (JOURNAL_EVERY_CALL, lib/runtime/native/checkpoint.ts) takes
|
|
38
|
+
* each event as its call ends, the first carrying the pin, and treats each
|
|
39
|
+
* as a checkpoint boundary, so a worker killed mid-script resumes from its
|
|
40
|
+
* last call. A resumed run that ends, and a durable run that finishes,
|
|
41
|
+
* store an `end` marker.
|
|
42
|
+
*
|
|
43
|
+
* THE SPANS (item 16). `script.invoke <name>` around the run and
|
|
44
|
+
* `script.call <kind>:<name>` around each live call, in their own trace,
|
|
45
|
+
* with no argument and no result on them; each agent() call's own turn
|
|
46
|
+
* span carries `script.run` and `script.call.seq` (the host adds them), so
|
|
47
|
+
* a trace view pairs them.
|
|
48
|
+
*/
|
|
49
|
+
import type { TurnEvent } from '../runtime/events.ts';
|
|
50
|
+
import type { PendingApproval } from '../runtime/approvals.ts';
|
|
51
|
+
import { SandboxHalt } from '../sandbox/sandbox.ts';
|
|
52
|
+
import type { SandboxLimits } from '../sandbox/sandbox.ts';
|
|
53
|
+
import type { PendingInput } from '../workflowConfig.ts';
|
|
54
|
+
import type { JournalEntry } from './journal.ts';
|
|
55
|
+
export { openRun, scriptPauseIn, SCRIPT_CALL, SCRIPT_PAUSE, SCRIPT_RUN } from './journal.ts';
|
|
56
|
+
export type { JournalEntry, OpenRun, ScriptCallKind, ScriptCallOutcome, ScriptCallRecord, ScriptPauseRecord, ScriptPinRecord } from './journal.ts';
|
|
57
|
+
/** The most calls one run may journal (item 13), as a walk has NODE_RUN_LIMIT. */
|
|
58
|
+
export declare const SCRIPT_CALL_LIMIT = 1000;
|
|
59
|
+
/** Where a call sits in its run: what the host stamps on an agent's span and lane. */
|
|
60
|
+
export interface ScriptCallSite {
|
|
61
|
+
run: string;
|
|
62
|
+
seq: number;
|
|
63
|
+
}
|
|
64
|
+
/** What the turn runner gives a run. */
|
|
65
|
+
export interface ScriptHost {
|
|
66
|
+
/** The script's name: every journal event's author and span's. */
|
|
67
|
+
script: string;
|
|
68
|
+
invocationId: string;
|
|
69
|
+
/** The conversation, for the spans. */
|
|
70
|
+
sessionId?: string;
|
|
71
|
+
/** Runs a declared agent; a plain Error is thrown into the script, a SandboxHalt ends the run. */
|
|
72
|
+
agent(name: string, input: unknown, opts: Record<string, unknown>, site: ScriptCallSite): Promise<unknown>;
|
|
73
|
+
/** Runs an allowed registry tool; errors as for agent. */
|
|
74
|
+
tool(name: string, args: unknown, site: ScriptCallSite): Promise<unknown>;
|
|
75
|
+
/** Whether a tool's calls wait for a person's approval (`script.require_approval`). */
|
|
76
|
+
gated?(name: string): boolean;
|
|
77
|
+
/** Appends a journal event to the conversation. */
|
|
78
|
+
append(event: TurnEvent): Promise<void>;
|
|
79
|
+
/** Every journal event as its call ends (a durable run), not only when the run pauses. */
|
|
80
|
+
journalEveryCall?: boolean;
|
|
81
|
+
/** Called once, before a pause is stored. */
|
|
82
|
+
beforePause?(): Promise<void>;
|
|
83
|
+
/** A `[STATUS]` progress line. */
|
|
84
|
+
log(text: string): void;
|
|
85
|
+
signal: AbortSignal;
|
|
86
|
+
newInterruptId?: () => string;
|
|
87
|
+
}
|
|
88
|
+
export interface ScriptRunInput {
|
|
89
|
+
source: string;
|
|
90
|
+
message: string;
|
|
91
|
+
transcript: string;
|
|
92
|
+
variables: Record<string, unknown>;
|
|
93
|
+
/**
|
|
94
|
+
* The run being resumed: its id and journal, and what the open pause
|
|
95
|
+
* takes: the person's answer to a question, or the decision on an
|
|
96
|
+
* approval. A run a stopped worker left (no pause) takes neither.
|
|
97
|
+
*/
|
|
98
|
+
resume?: {
|
|
99
|
+
run: string;
|
|
100
|
+
entries: JournalEntry[];
|
|
101
|
+
answer?: string;
|
|
102
|
+
decision?: {
|
|
103
|
+
approved: boolean;
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
callLimit?: number;
|
|
107
|
+
limits?: Partial<SandboxLimits>;
|
|
108
|
+
}
|
|
109
|
+
export type ScriptRunOutcome = {
|
|
110
|
+
status: 'completed';
|
|
111
|
+
run: string;
|
|
112
|
+
value: unknown;
|
|
113
|
+
logs: string;
|
|
114
|
+
} | {
|
|
115
|
+
status: 'paused';
|
|
116
|
+
run: string;
|
|
117
|
+
input: PendingInput;
|
|
118
|
+
logs: string;
|
|
119
|
+
} | {
|
|
120
|
+
status: 'approval';
|
|
121
|
+
run: string;
|
|
122
|
+
approval: PendingApproval;
|
|
123
|
+
logs: string;
|
|
124
|
+
} | {
|
|
125
|
+
status: 'failed';
|
|
126
|
+
run: string;
|
|
127
|
+
error: {
|
|
128
|
+
code: string;
|
|
129
|
+
message: string;
|
|
130
|
+
};
|
|
131
|
+
logs: string;
|
|
132
|
+
};
|
|
133
|
+
/** Fatal: thrown by a host function to fail the turn with a code. */
|
|
134
|
+
export declare class ScriptFatal extends SandboxHalt {
|
|
135
|
+
readonly code: string;
|
|
136
|
+
constructor(code: string, message: string);
|
|
137
|
+
}
|
|
138
|
+
export declare function runScript(input: ScriptRunInput, host: ScriptHost): Promise<ScriptRunOutcome>;
|