@particle-academy/prism-harness 0.0.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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +179 -0
  3. package/dist/doctor.d.ts +35 -0
  4. package/dist/doctor.js +110 -0
  5. package/dist/errors.d.ts +199 -0
  6. package/dist/errors.js +201 -0
  7. package/dist/events.d.ts +67 -0
  8. package/dist/events.js +40 -0
  9. package/dist/harness.d.ts +51 -0
  10. package/dist/harness.js +70 -0
  11. package/dist/index.d.ts +36 -0
  12. package/dist/index.js +20 -0
  13. package/dist/json.d.ts +15 -0
  14. package/dist/json.js +15 -0
  15. package/dist/modes.d.ts +63 -0
  16. package/dist/modes.js +109 -0
  17. package/dist/runtime.d.ts +104 -0
  18. package/dist/runtime.js +244 -0
  19. package/dist/session.d.ts +114 -0
  20. package/dist/session.js +198 -0
  21. package/dist/skills.d.ts +37 -0
  22. package/dist/skills.js +73 -0
  23. package/dist/store-task-source.d.ts +169 -0
  24. package/dist/store-task-source.js +513 -0
  25. package/dist/stores/file-session-store.d.ts +34 -0
  26. package/dist/stores/file-session-store.js +284 -0
  27. package/dist/stores/memory-session-store.d.ts +22 -0
  28. package/dist/stores/memory-session-store.js +97 -0
  29. package/dist/stores/session-store.d.ts +65 -0
  30. package/dist/stores/session-store.js +3 -0
  31. package/dist/stores/store-manager.d.ts +32 -0
  32. package/dist/stores/store-manager.js +57 -0
  33. package/dist/subagent-runner.d.ts +45 -0
  34. package/dist/subagent-runner.js +89 -0
  35. package/dist/subagents.d.ts +182 -0
  36. package/dist/subagents.js +286 -0
  37. package/dist/task-tools.d.ts +55 -0
  38. package/dist/task-tools.js +136 -0
  39. package/dist/tasks.d.ts +257 -0
  40. package/dist/tasks.js +162 -0
  41. package/dist/thread.d.ts +49 -0
  42. package/dist/thread.js +88 -0
  43. package/dist/tools.d.ts +89 -0
  44. package/dist/tools.js +148 -0
  45. package/package.json +34 -0
package/dist/modes.js ADDED
@@ -0,0 +1,109 @@
1
+ import { HarnessError } from './errors.js';
2
+ import { subagentFromConfig } from './subagents.js';
3
+ /**
4
+ * What a mode lets a run do.
5
+ *
6
+ * A mode is the unit of authority here: the tools it may reach, the skills it
7
+ * loads, how many steps it gets, which nested agents it may call, and which of
8
+ * its tools need a human first.
9
+ */
10
+ export class AgentMode {
11
+ name;
12
+ systemPrompt;
13
+ tools;
14
+ skills;
15
+ maxSteps;
16
+ subagents;
17
+ requiresApproval;
18
+ constructor(name, systemPrompt, tools, skills, maxSteps,
19
+ /** Nested agents this mode may call, by name. */
20
+ subagents = {},
21
+ /** Tools that must not run until a human says so. */
22
+ requiresApproval = []) {
23
+ this.name = name;
24
+ this.systemPrompt = systemPrompt;
25
+ this.tools = tools;
26
+ this.skills = skills;
27
+ this.maxSteps = maxSteps;
28
+ this.subagents = subagents;
29
+ this.requiresApproval = requiresApproval;
30
+ }
31
+ /**
32
+ * Whether a named tool needs a human before it runs IN THIS MODE.
33
+ *
34
+ * Declared per mode rather than on the tool, because the same tool is not
35
+ * equally consequential everywhere: `execute_op` against a scratch project is
36
+ * routine and against production is not, and the tool cannot tell which it is
37
+ * in. `'*'` gates every tool the mode offers.
38
+ */
39
+ needsApproval(tool) {
40
+ return this.requiresApproval.includes('*') || this.requiresApproval.includes(tool);
41
+ }
42
+ }
43
+ export class ModeRegistry {
44
+ config;
45
+ constructor(config = {}) {
46
+ this.config = config;
47
+ }
48
+ default() {
49
+ const configured = this.config.default;
50
+ return typeof configured === 'string' && configured !== '' ? configured : 'chat';
51
+ }
52
+ /** Every mode name this application has configured. */
53
+ names() {
54
+ return Object.keys(this.config.modes ?? {});
55
+ }
56
+ /**
57
+ * Every mode, RESOLVED.
58
+ *
59
+ * Resolving them all is the point: `resolve()` validates as it goes, so a
60
+ * mode nobody has entered yet keeps its misconfiguration until the day
61
+ * somebody switches to it. This is what the doctor uses to find that on a
62
+ * Tuesday rather than in front of a user.
63
+ */
64
+ all() {
65
+ return Object.fromEntries(this.names().map((name) => [name, this.resolve(name)]));
66
+ }
67
+ resolve(name) {
68
+ const wanted = name ?? this.default();
69
+ const mode = this.config.modes?.[wanted];
70
+ if (mode === undefined) {
71
+ throw HarnessError.modeNotConfigured(wanted);
72
+ }
73
+ const prompt = mode.system_prompt ?? '';
74
+ const maxSteps = mode.max_steps ?? 8;
75
+ if (typeof prompt !== 'string' || !Number.isInteger(maxSteps) || maxSteps < 1) {
76
+ throw HarnessError.modeMalformed(wanted, 'system_prompt must be a string and max_steps a positive integer');
77
+ }
78
+ return new AgentMode(wanted, prompt, strings(mode.tools), strings(mode.skills), maxSteps, this.#subagentsFor(wanted, mode), strings(mode.requires_approval));
79
+ }
80
+ /**
81
+ * The nested agents a mode may call.
82
+ *
83
+ * DECLARED PER MODE, which is the point: a subagent is authority, and
84
+ * authority a run inherits by being nested is authority nobody granted. A
85
+ * mode that names no subagents cannot spawn one.
86
+ *
87
+ * A subagent's own mode must EXIST, and it is checked here rather than at
88
+ * call time so a typo surfaces when the parent mode is loaded instead of
89
+ * halfway through a run that has already spent budget.
90
+ */
91
+ #subagentsFor(name, mode) {
92
+ const declared = mode.subagents ?? {};
93
+ const subagents = {};
94
+ for (const [key, config] of Object.entries(declared)) {
95
+ if (typeof config !== 'object' || config === null || Array.isArray(config)) {
96
+ throw HarnessError.modeMalformed(name, `subagent [${key}] is not an object`);
97
+ }
98
+ const subagent = subagentFromConfig(key, config);
99
+ if (this.config.modes?.[subagent.mode] === undefined) {
100
+ throw HarnessError.modeMalformed(name, `it declares subagent [${key}], whose mode [${subagent.mode}] is not configured`);
101
+ }
102
+ subagents[key] = subagent;
103
+ }
104
+ return subagents;
105
+ }
106
+ }
107
+ function strings(value) {
108
+ return (value ?? []).filter((entry) => typeof entry === 'string');
109
+ }
@@ -0,0 +1,104 @@
1
+ import type { JsonObject } from './json.js';
2
+ import type { HarnessEvents } from './events.js';
3
+ import type { ModeRegistry } from './modes.js';
4
+ import type { Session } from './session.js';
5
+ import { RunContext } from './subagents.js';
6
+ import type { HarnessTool, ToolAuthorizer, ToolRegistry } from './tools.js';
7
+ /**
8
+ * What the runtime needs from a model, and NOTHING MORE.
9
+ *
10
+ * An INTERFACE rather than a dependency on `prism-ts`. The loop below — steps,
11
+ * budgets, approvals, thread recording, events — is the part worth porting, and
12
+ * none of it needs to know how a request reaches a provider. Keeping the seam
13
+ * here also means this package stays at zero dependencies and a consumer can
14
+ * drive it with `prism-ts`, with their own client, or with a fake in a test.
15
+ *
16
+ * The reference couples these because Prism is already a dependency there.
17
+ */
18
+ export interface LlmRequest {
19
+ systemPrompt: string;
20
+ /** The conversation so far, serialized — oldest first. */
21
+ messages: readonly JsonObject[];
22
+ tools: readonly HarnessTool[];
23
+ provider: string;
24
+ model: string;
25
+ }
26
+ export interface LlmToolCall {
27
+ id: string;
28
+ name: string;
29
+ arguments: JsonObject;
30
+ }
31
+ export interface LlmResponse {
32
+ text: string;
33
+ toolCalls?: readonly LlmToolCall[];
34
+ /** `stop`, `tool_calls`, `length`, … — the provider's own reason, passed through. */
35
+ finishReason: string;
36
+ /** Null when the provider does not report one. NOT zero — see `RunLedger.recordCost`. */
37
+ costUsd?: number | null;
38
+ }
39
+ export type LlmClient = (request: LlmRequest) => Promise<LlmResponse>;
40
+ export interface AgentResponse {
41
+ runId: string;
42
+ text: string;
43
+ steps: number;
44
+ /** NAMES only, in call order. */
45
+ toolCalls: readonly string[];
46
+ finishReason: string;
47
+ /** Set when the run stopped because a tool needs a human. */
48
+ pendingApprovals: readonly PendingApproval[];
49
+ /** Set when the run stopped because the tree ran out of budget, or was cancelled. */
50
+ stoppedBecause: string | null;
51
+ }
52
+ export interface PendingApproval {
53
+ id: string;
54
+ tool: string;
55
+ arguments: JsonObject;
56
+ }
57
+ export interface AgentRuntimeOptions {
58
+ client: LlmClient;
59
+ modes: ModeRegistry;
60
+ tools: ToolRegistry;
61
+ authorizer?: ToolAuthorizer;
62
+ events?: HarnessEvents;
63
+ }
64
+ /**
65
+ * The loop: prompt in, turns out, everything recorded.
66
+ *
67
+ * Three properties matter more than the mechanics.
68
+ *
69
+ * **Every step is checked against the budget BEFORE it is taken**, not after.
70
+ * Checking afterwards means the step that broke the limit has already been
71
+ * paid for, which makes a budget a report rather than a control.
72
+ *
73
+ * **An approval stops the run and is written to the THREAD**, not held in
74
+ * memory. That is what makes it survive: the approval a person grants this
75
+ * morning is a durable row, so the worker that resumes tonight — a different
76
+ * process, possibly after a deploy — reads the same answer.
77
+ *
78
+ * **A tool that needs approval and has none DOES NOT RUN.** Failing closed is
79
+ * the only safe direction: an unanswered approval that executed anyway is
80
+ * exactly the outcome the whole mechanism exists to prevent.
81
+ */
82
+ export declare class AgentRuntime {
83
+ #private;
84
+ constructor(options: AgentRuntimeOptions);
85
+ /**
86
+ * Run a turn.
87
+ *
88
+ * An EMPTY prompt is meaningful and not an error: it is how a run resumes
89
+ * after an approval, because the conversation already contains the request,
90
+ * the decision, and everything before them. A new prompt there would be a
91
+ * second instruction competing with the one the tool call came from.
92
+ */
93
+ send(session: Session, prompt: string, toolNames?: readonly string[], context?: RunContext): Promise<AgentResponse>;
94
+ }
95
+ /**
96
+ * Answer a pending approval, durably.
97
+ *
98
+ * The decision is RECORDED IN THE THREAD, not held anywhere else. Who may
99
+ * approve is the APPLICATION's decision, not this package's: the session is
100
+ * already scoped to a participant, so nobody can answer another participant's
101
+ * approval through it, but "this user may approve THIS action" is a question
102
+ * only the host can answer. Authorize before calling.
103
+ */
104
+ export declare function recordApproval(session: Session, approvalId: string, approved: boolean, reason?: string | null): Promise<void>;
@@ -0,0 +1,244 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { HarnessError } from './errors.js';
3
+ import { RunBudget, RunContext } from './subagents.js';
4
+ /**
5
+ * The loop: prompt in, turns out, everything recorded.
6
+ *
7
+ * Three properties matter more than the mechanics.
8
+ *
9
+ * **Every step is checked against the budget BEFORE it is taken**, not after.
10
+ * Checking afterwards means the step that broke the limit has already been
11
+ * paid for, which makes a budget a report rather than a control.
12
+ *
13
+ * **An approval stops the run and is written to the THREAD**, not held in
14
+ * memory. That is what makes it survive: the approval a person grants this
15
+ * morning is a durable row, so the worker that resumes tonight — a different
16
+ * process, possibly after a deploy — reads the same answer.
17
+ *
18
+ * **A tool that needs approval and has none DOES NOT RUN.** Failing closed is
19
+ * the only safe direction: an unanswered approval that executed anyway is
20
+ * exactly the outcome the whole mechanism exists to prevent.
21
+ */
22
+ export class AgentRuntime {
23
+ #client;
24
+ #modes;
25
+ #tools;
26
+ #authorizer;
27
+ #events;
28
+ constructor(options) {
29
+ this.#client = options.client;
30
+ this.#modes = options.modes;
31
+ this.#tools = options.tools;
32
+ this.#authorizer = options.authorizer;
33
+ this.#events = options.events;
34
+ }
35
+ /**
36
+ * Run a turn.
37
+ *
38
+ * An EMPTY prompt is meaningful and not an error: it is how a run resumes
39
+ * after an approval, because the conversation already contains the request,
40
+ * the decision, and everything before them. A new prompt there would be a
41
+ * second instruction competing with the one the tool call came from.
42
+ */
43
+ async send(session, prompt, toolNames, context) {
44
+ const mode = this.#modes.resolve(await session.mode());
45
+ const provider = (await session.provider()) ?? 'unknown';
46
+ const model = (await session.model()) ?? 'unknown';
47
+ const runId = randomUUID();
48
+ const run = context ?? RunContext.root(runId, new RunBudget(mode.maxSteps));
49
+ const thread = session.thread();
50
+ if (run.tooDeep()) {
51
+ throw HarnessError.runNotPermitted(`This run is nested ${run.depth} deep, at or past the ceiling. Nobody debugs a tree that deep, ` +
52
+ 'and a configuration that produced one is a mistake worth reporting rather than executing.');
53
+ }
54
+ await session.beginRun(runId, mode.name, provider, model);
55
+ this.#events?.emit({
56
+ type: 'run.started',
57
+ runId,
58
+ sessionKey: session.key(),
59
+ mode: mode.name,
60
+ provider,
61
+ model,
62
+ rootRunId: run.rootRunId,
63
+ depth: run.depth,
64
+ at: new Date().toISOString(),
65
+ });
66
+ if (prompt !== '') {
67
+ await thread.record([{ type: 'user', content: prompt }], runId);
68
+ }
69
+ try {
70
+ return await this.#loop(session, mode, run, runId, provider, model, toolNames);
71
+ }
72
+ catch (error) {
73
+ const failure = error instanceof Error ? error.message : String(error);
74
+ await session.failRun(runId, failure);
75
+ this.#events?.emit({
76
+ type: 'run.failed',
77
+ runId,
78
+ sessionKey: session.key(),
79
+ failure,
80
+ steps: run.ledger.steps,
81
+ at: new Date().toISOString(),
82
+ });
83
+ throw error;
84
+ }
85
+ }
86
+ async #loop(session, mode, run, runId, provider, model, toolNames) {
87
+ const thread = session.thread();
88
+ const resolved = await this.#tools.resolve(toolNames ?? mode.tools, session);
89
+ const offered = this.#authorizer
90
+ ? await this.#authorizer.allowed(session, resolved)
91
+ : [...resolved.values()];
92
+ const called = [];
93
+ let text = '';
94
+ let finishReason = 'stop';
95
+ for (;;) {
96
+ // BEFORE the step, never after. Checking afterwards means the step that
97
+ // broke the limit has already been paid for.
98
+ const exhausted = run.ledger.exhaustion(run.budget);
99
+ if (exhausted !== null) {
100
+ return await this.#finish(session, runId, called, 'budget_exhausted', text, run, exhausted);
101
+ }
102
+ const response = await this.#client({
103
+ systemPrompt: mode.systemPrompt,
104
+ messages: (await thread.messages()).map((entry) => entry.message),
105
+ tools: offered,
106
+ provider,
107
+ model,
108
+ });
109
+ run.ledger.recordSteps(1);
110
+ run.ledger.recordCost(response.costUsd ?? null);
111
+ text = response.text;
112
+ finishReason = response.finishReason;
113
+ const toolCalls = response.toolCalls ?? [];
114
+ await thread.record([
115
+ {
116
+ type: 'assistant',
117
+ content: response.text,
118
+ tool_calls: toolCalls.map((call) => ({ id: call.id, name: call.name })),
119
+ },
120
+ ], runId);
121
+ if (toolCalls.length === 0) {
122
+ return await this.#finish(session, runId, called, finishReason, text, run, null);
123
+ }
124
+ const pending = await this.#pendingApprovals(session, mode, toolCalls);
125
+ if (pending.length > 0) {
126
+ // FAILS CLOSED. The run stops here and the request is already in the
127
+ // thread, so a different process can pick it up after a human answers.
128
+ await thread.record([{ type: 'tool_approval_request', approvals: pending }], runId);
129
+ return {
130
+ runId,
131
+ text,
132
+ steps: run.ledger.steps,
133
+ toolCalls: called,
134
+ finishReason: 'awaiting_approval',
135
+ pendingApprovals: pending,
136
+ stoppedBecause: null,
137
+ };
138
+ }
139
+ for (const call of toolCalls) {
140
+ called.push(call.name);
141
+ await thread.record([await this.#invoke(offered, call)], runId);
142
+ }
143
+ }
144
+ }
145
+ /**
146
+ * Which of these calls needs a human, and has not had one.
147
+ *
148
+ * An approval already answered in the thread is NOT asked again — that is the
149
+ * whole point of recording it durably. An answered-and-denied approval is
150
+ * also not asked again; it is simply not executed.
151
+ */
152
+ async #pendingApprovals(session, mode, toolCalls) {
153
+ const gated = toolCalls.filter((call) => mode.needsApproval(call.name));
154
+ if (gated.length === 0)
155
+ return [];
156
+ const answered = await this.#answeredApprovals(session);
157
+ return gated
158
+ .filter((call) => !answered.has(call.id))
159
+ .map((call) => ({ id: call.id, tool: call.name, arguments: call.arguments }));
160
+ }
161
+ async #answeredApprovals(session) {
162
+ const answered = new Map();
163
+ for (const entry of await session.thread().messages()) {
164
+ if (entry.message.type !== 'tool_approval_response')
165
+ continue;
166
+ const id = entry.message.approval_id;
167
+ const approved = entry.message.approved;
168
+ if (typeof id === 'string')
169
+ answered.set(id, approved === true);
170
+ }
171
+ return answered;
172
+ }
173
+ async #invoke(offered, call) {
174
+ const tool = offered.find((candidate) => candidate.name === call.name);
175
+ if (tool === undefined) {
176
+ throw HarnessError.toolNotAvailable(call.name, offered.map((candidate) => candidate.name));
177
+ }
178
+ try {
179
+ const result = await tool.handle(call.arguments);
180
+ return {
181
+ type: 'tool_result',
182
+ tool_call_id: call.id,
183
+ name: call.name,
184
+ result: typeof result === 'string' ? result : JSON.stringify(result ?? null),
185
+ };
186
+ }
187
+ catch (error) {
188
+ // A failed tool is a RESULT, not a crashed run: the model can often
189
+ // recover, and losing the whole turn to one bad call is worse. A refused
190
+ // call is different and is left to propagate — see `authorizedTool`.
191
+ if (error instanceof HarnessError && error.code === 'call_not_authorized')
192
+ throw error;
193
+ return {
194
+ type: 'tool_result',
195
+ tool_call_id: call.id,
196
+ name: call.name,
197
+ result: `The tool failed: ${error instanceof Error ? error.message : String(error)}`,
198
+ failed: true,
199
+ };
200
+ }
201
+ }
202
+ async #finish(session, runId, called, finishReason, text, run, stoppedBecause) {
203
+ await session.completeRun(runId, finishReason, called);
204
+ this.#events?.emit({
205
+ type: 'run.finished',
206
+ runId,
207
+ sessionKey: session.key(),
208
+ finishReason,
209
+ toolCalls: called,
210
+ steps: run.ledger.steps,
211
+ costUsd: run.ledger.unmeteredRuns > 0 ? null : run.ledger.costUsd,
212
+ at: new Date().toISOString(),
213
+ });
214
+ return {
215
+ runId,
216
+ text,
217
+ steps: run.ledger.steps,
218
+ toolCalls: called,
219
+ finishReason,
220
+ pendingApprovals: [],
221
+ stoppedBecause,
222
+ };
223
+ }
224
+ }
225
+ /**
226
+ * Answer a pending approval, durably.
227
+ *
228
+ * The decision is RECORDED IN THE THREAD, not held anywhere else. Who may
229
+ * approve is the APPLICATION's decision, not this package's: the session is
230
+ * already scoped to a participant, so nobody can answer another participant's
231
+ * approval through it, but "this user may approve THIS action" is a question
232
+ * only the host can answer. Authorize before calling.
233
+ */
234
+ export async function recordApproval(session, approvalId, approved, reason = null) {
235
+ const run = await session.run();
236
+ await session.thread().record([
237
+ {
238
+ type: 'tool_approval_response',
239
+ approval_id: approvalId,
240
+ approved,
241
+ reason,
242
+ },
243
+ ], run?.id ?? null);
244
+ }
@@ -0,0 +1,114 @@
1
+ import type { JsonObject } from './json.js';
2
+ import type { SessionStore } from './stores/session-store.js';
3
+ import { StoreTaskSource } from './store-task-source.js';
4
+ import type { StoreTaskSourceOptions } from './store-task-source.js';
5
+ import { Thread } from './thread.js';
6
+ /**
7
+ * Who a session belongs to.
8
+ *
9
+ * A TYPE and an ID, not just an id: one application holds users and teams and
10
+ * bots, and `7` means a different participant in each. The reference gets the
11
+ * same pair from Eloquent's morph class and primary key.
12
+ */
13
+ export interface Participant {
14
+ type: string;
15
+ id: string | number;
16
+ }
17
+ export interface RunState extends JsonObject {
18
+ id: string;
19
+ status: 'running' | 'completed' | 'failed';
20
+ mode?: string;
21
+ provider?: string;
22
+ model?: string;
23
+ started_at?: string;
24
+ }
25
+ export interface SessionOptions {
26
+ participant: Participant;
27
+ scope: string;
28
+ ephemeral: SessionStore;
29
+ durable: SessionStore;
30
+ /** How long ephemeral state lives. Null keeps it until forgotten. */
31
+ ttlSeconds?: number | null;
32
+ }
33
+ /**
34
+ * One participant's live runtime, reconstructed per turn.
35
+ *
36
+ * RESOLVED, NEVER HELD. Nothing survives in memory between turns, so a fresh
37
+ * worker resolving the same address has to see the same active mode, the same
38
+ * model and the same conversation as whatever set them. Everything here reads
39
+ * through a store for that reason, rather than being a property that happens to
40
+ * be populated.
41
+ *
42
+ * State is split deliberately:
43
+ *
44
+ * - mode, model, provider and run bookkeeping are EPHEMERAL. Lose them and the
45
+ * next turn falls back to a default, which is a shrug.
46
+ * - the thread and stored capabilities are DURABLE.
47
+ */
48
+ export declare class Session {
49
+ #private;
50
+ readonly scope: string;
51
+ readonly participant: Participant;
52
+ constructor(options: SessionOptions);
53
+ /**
54
+ * The address this session is resolved by.
55
+ *
56
+ * Participant AND scope, because one participant holds several unrelated
57
+ * conversations at once and they must not collide.
58
+ *
59
+ * The type is HASHED rather than interpolated, and with sha1 truncated to 12
60
+ * characters — byte for byte what the PHP reference produces. A class name
61
+ * contains backslashes there, which make for awkward store keys and leak the
62
+ * application's namespace layout into something visible in tooling. Matching
63
+ * the reference exactly is what lets a PHP app and a TypeScript agent share
64
+ * ONE store and resolve the same session.
65
+ */
66
+ key(): string;
67
+ mode(): Promise<string | null>;
68
+ usingMode(mode: string): Promise<this>;
69
+ model(): Promise<string | null>;
70
+ usingModel(model: string): Promise<this>;
71
+ provider(): Promise<string | null>;
72
+ usingProvider(provider: string): Promise<this>;
73
+ state(): Promise<JsonObject>;
74
+ /** Drop the ephemeral half. THE CONVERSATION IS UNTOUCHED. */
75
+ forget(): Promise<this>;
76
+ /** The stored conversation this session is bound to. */
77
+ thread(): Thread;
78
+ /**
79
+ * The agent task list this session is bound to.
80
+ *
81
+ * The DURABLE half, alongside the thread, for the same reason: a
82
+ * half-finished task list that vanishes on a deploy is indistinguishable from
83
+ * a finished one. A volatile durable slot has already been refused by the
84
+ * store manager before this can be reached, and `StoreTaskSource` refuses one
85
+ * again on its own -- the second check is what makes a source built directly
86
+ * against a store as safe as one reached through here.
87
+ */
88
+ tasks(options?: StoreTaskSourceOptions): StoreTaskSource;
89
+ capability(name: string): Promise<JsonObject | null>;
90
+ usingCapability(name: string, state: JsonObject): Promise<this>;
91
+ forgetCapability(name: string): Promise<this>;
92
+ run(): Promise<RunState | null>;
93
+ beginRun(id: string, mode: string, provider: string, model: string): Promise<this>;
94
+ /**
95
+ * @param toolCalls the NAMES of the tools this run invoked, in order.
96
+ *
97
+ * NAMES ONLY, and that boundary is deliberate. "Which tools did this run
98
+ * reach for" is what an operator needs to audit a guardrail, and a tool name
99
+ * is not PII. ARGUMENTS are — `prism-opentelemetry` already carries them
100
+ * behind an opt-in capture gate with a length cap, and recording them a
101
+ * second time here, ungated, would quietly undo that decision for everyone
102
+ * who installed both.
103
+ */
104
+ completeRun(id: string, finishReason: string, toolCalls?: readonly string[]): Promise<this>;
105
+ failRun(id: string, failure: string): Promise<this>;
106
+ /**
107
+ * Run something that MUST NOT HAPPEN TWICE.
108
+ *
109
+ * Two workers can hold the same session at the same moment: a queued job
110
+ * finishing a run while the user sends another message is ordinary. Advance a
111
+ * run inside this, not outside it.
112
+ */
113
+ lock<T>(callback: (session: this) => T | Promise<T>, ttlSeconds?: number, waitSeconds?: number): Promise<T>;
114
+ }