@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/errors.js ADDED
@@ -0,0 +1,201 @@
1
+ export class HarnessError extends Error {
2
+ code;
3
+ constructor(code, message, options = {}) {
4
+ super(message, options);
5
+ this.name = 'HarnessError';
6
+ this.code = code;
7
+ }
8
+ static sessionLocked(key, waitSeconds) {
9
+ return new HarnessError('session_locked', `Could not acquire the lock on session [${key}] within ${waitSeconds}s. ` +
10
+ 'Another worker is holding it; the callback was NOT run.');
11
+ }
12
+ /**
13
+ * The guard this package exists for.
14
+ *
15
+ * A cache is disposable by definition, and the durable slot holds pending
16
+ * tool approvals — a half-executed action waiting on a human. Losing one is a
17
+ * correctness failure, not a cache miss, so a store that reports itself
18
+ * volatile is refused here rather than accepted and discovered later.
19
+ */
20
+ static volatileDurableStore(slot, driver) {
21
+ return new HarnessError('unsafe_state_configuration', `The [${slot}] slot is configured with the [${driver}] driver, which reports itself VOLATILE. ` +
22
+ 'Durable state (threads, pending tool approvals) must survive a deploy. Either point this ' +
23
+ 'slot at a durable driver, or — if this store really does persist — declare it durable when ' +
24
+ 'you register it. That declaration is an assertion about your infrastructure, not a preference.');
25
+ }
26
+ static unknownStoreDriver(slot, driver) {
27
+ return new HarnessError('unknown_store_driver', `The [${slot}] slot names the driver [${driver}], which is not registered.`);
28
+ }
29
+ static toolNotAvailable(name, available) {
30
+ return new HarnessError('tool_not_available', `The tool [${name}] is not available to this session (has: ${available.join(', ') || 'none'}).`);
31
+ }
32
+ static runNotPermitted(reason) {
33
+ return new HarnessError('run_not_permitted', reason);
34
+ }
35
+ static unmappableContent(description, options = {}) {
36
+ return new HarnessError('unmappable_content', `Could not rebuild stored content: ${description}.`, options);
37
+ }
38
+ static modeNotConfigured(name) {
39
+ return new HarnessError('mode_not_configured', `Harness mode [${name}] is not configured.`);
40
+ }
41
+ static modeMalformed(name, detail) {
42
+ return new HarnessError('mode_malformed', `Harness mode [${name}] is malformed: ${detail}.`);
43
+ }
44
+ /**
45
+ * Both at once is the one configuration not to leave in place.
46
+ *
47
+ * A defined policy that is never consulted looks like a control to every
48
+ * reader and is not one -- every registered tool is offered to every run
49
+ * while the code says otherwise.
50
+ */
51
+ static policyDefinedButDisabled() {
52
+ return new HarnessError('unsafe_authorization_configuration', 'A tool authorization policy was supplied, but the authorizer is disabled, so that policy is ' +
53
+ 'never consulted and every registered tool is offered to every run. Either enable the ' +
54
+ 'authorizer, or remove the policy so nothing suggests tool access is being restricted.');
55
+ }
56
+ /**
57
+ * Thrown rather than returned as a tool result.
58
+ *
59
+ * A refusal handed back as a result reads to the model as a failure it might
60
+ * retry differently, and a denied action being retried is the opposite of
61
+ * what a guard is for.
62
+ */
63
+ static callNotAuthorized(tool) {
64
+ return new HarnessError('call_not_authorized', `This call to [${tool}] was refused by the tool authorization policy.`);
65
+ }
66
+ /**
67
+ * A skill name or path that would leave the skill directory.
68
+ *
69
+ * Refused rather than sanitised. Silently rewriting a traversal to something
70
+ * safe teaches a caller — or a model — that the request was fine.
71
+ */
72
+ static skillPathRefused(detail) {
73
+ return new HarnessError('skill_path_refused', `Refused to read a skill file: ${detail}.`);
74
+ }
75
+ /**
76
+ * A task list pointed at a store whose contents can vanish.
77
+ *
78
+ * The SAME code as `volatileDurableStore`, deliberately. It is the same
79
+ * misconfiguration reaching the same conclusion, and a consumer branching on
80
+ * "durable state was pointed somewhere it cannot live" should not have to
81
+ * know which feature noticed. Prose is free per prism-parity decision 0004;
82
+ * the code is not.
83
+ *
84
+ * A half-finished task list that vanishes on a deploy is indistinguishable
85
+ * from a finished one, which is why this refuses at construction rather than
86
+ * degrading to a default.
87
+ */
88
+ static volatileTaskSource() {
89
+ return new HarnessError('unsafe_state_configuration', 'An agent task list was pointed at a store that reports itself VOLATILE. The list is durable ' +
90
+ 'state: losing it is a correctness failure, because a half-finished list that vanishes on a ' +
91
+ 'deploy looks exactly like a finished one. Point it at a durable store, or -- if this store ' +
92
+ 'really does persist -- declare it durable when you register it. That declaration is an ' +
93
+ 'assertion about your infrastructure, not a preference.');
94
+ }
95
+ /**
96
+ * A lease that is not a whole positive number of seconds. REFUSED, not adjusted.
97
+ *
98
+ * Both halves were learned the hard way. Zero or less was CLAMPED to one
99
+ * second at first, because a clamp fails closed; a positive fraction was then
100
+ * still TRUNCATED. Both are a value quietly becoming a different value, and
101
+ * this repository has already shipped a configuration that did exactly that
102
+ * and stayed green throughout. A caller asking for a zero-second or
103
+ * fractional lease has a bug, and the useful thing to hand back is the bug.
104
+ *
105
+ * Takes `unknown` rather than `number` deliberately: the value that reaches
106
+ * this is whatever actually arrived, including a string out of a JSON config
107
+ * that the type annotation never saw.
108
+ */
109
+ static taskLeaseInvalid(seconds) {
110
+ return new HarnessError('task_lease_invalid', `A lease of [${String(seconds)}] seconds was asked for. It must be a whole number of seconds, ` +
111
+ 'at least one. A fraction cannot be honoured -- the expiry is an integer timestamp -- and a ' +
112
+ 'lease that has already expired when it is granted hands the same task to a second worker.');
113
+ }
114
+ static taskNotFound(id) {
115
+ return new HarnessError('task_not_found', `No task with the id [${id}] is in this source.`);
116
+ }
117
+ static duplicateTaskId(id) {
118
+ return new HarnessError('duplicate_task_id', `A task with the id [${id}] is already in this source. Ids are unique within a source, and ` +
119
+ 'reusing one would let two units of work share a claim.');
120
+ }
121
+ /**
122
+ * An empty identifier, compared against `""` EXACTLY.
123
+ *
124
+ * DELIBERATELY NOT TRIMMED, in any of the three languages. PHP's `trim`,
125
+ * JavaScript's `String.prototype.trim` and Python's `str.strip` each strip a
126
+ * different set of codepoints, so trimming here would produce three different
127
+ * answers for the same input -- which is precisely how `prism-human-plus`
128
+ * G-36 happened. A worker id of one space is accepted, and that is the
129
+ * correct behaviour rather than an oversight.
130
+ */
131
+ static taskIdentifierBlank(what) {
132
+ return new HarnessError('task_identifier_blank', `The ${what} is the empty string. An identifier that is not an identifier would make one ` +
133
+ 'claim indistinguishable from another.');
134
+ }
135
+ /**
136
+ * An error, never a silent no-op.
137
+ *
138
+ * A second `release()` means two things believe they own the outcome of one
139
+ * task, and swallowing it leaves whichever wrote last as the answer with
140
+ * nothing recording that they disagreed.
141
+ */
142
+ static taskAlreadyTerminal(id, state) {
143
+ return new HarnessError('task_already_terminal', `The task [${id}] is already [${state}], which is terminal. Re-releasing it is refused rather ` +
144
+ 'than ignored: two callers believing they own one outcome is worth reporting.');
145
+ }
146
+ /**
147
+ * "You are not holding this" -- and NOT WHO IS.
148
+ *
149
+ * The holder is deliberately left out of the sentence. This error can be
150
+ * raised inside a tool call, and a tool's failure comes back to the model as
151
+ * readable text: naming the other worker would hand an agent the identity of
152
+ * a peer it has no business knowing, from a refusal. The caller that needs to
153
+ * know who holds a task can ask the source.
154
+ *
155
+ * Covers three situations -- nobody holds it, someone else does, and your own
156
+ * lease expired -- because all three have the same answer: claim it again.
157
+ */
158
+ static taskLeaseNotHeld(id, worker, detail) {
159
+ return new HarnessError('task_lease_not_held', `The worker [${worker}] is not holding the task [${id}]: ${detail}. Claim it again rather ` +
160
+ 'than acting on it.');
161
+ }
162
+ /**
163
+ * An outcome that is not `done` or `failed` was named.
164
+ *
165
+ * REFUSED, not coerced. The outcome decides a TERMINAL state; mapping
166
+ * `"complete"`, `"DONE"`, `null` or a MISSING argument onto `done` would mean
167
+ * a malformed call produces the MORE privileged result -- an agent declaring
168
+ * victory by typo. Failing closed here costs a retry with a valid value.
169
+ *
170
+ * Absent is covered by the same code deliberately. The argument for treating
171
+ * it as `done` is real -- calling a tool named `complete_task` looks like the
172
+ * declaration by itself -- and the reference settled against it.
173
+ *
174
+ * `where` names the door that refused rather than always a tool, because
175
+ * there are two: the completion tool a model can call, and
176
+ * `StoreTaskSource.release()`, which is reachable from any untyped caller.
177
+ */
178
+ static taskOutcomeInvalid(where, given) {
179
+ return new HarnessError('task_outcome_invalid', `[${where}] was given the outcome [${String(given)}]. It must be exactly [done] ` +
180
+ 'or [failed]; anything else is refused rather than guessed at, because guessing would let a ' +
181
+ 'malformed argument close a task.');
182
+ }
183
+ /**
184
+ * A self-completion tool built against an authorizer that is switched off.
185
+ *
186
+ * The same code, and the same argument, as `policyDefinedButDisabled`: a
187
+ * control that is never consulted reads as a control to every reader and is
188
+ * not one. An agent closing its own tasks is exactly the authority that must
189
+ * not be granted by accident.
190
+ */
191
+ static completionToolWithoutAuthorizer(name) {
192
+ return new HarnessError('unsafe_authorization_configuration', `The tool [${name}] would let the agent mark its own task complete, but the ToolAuthorizer it ` +
193
+ 'was given is disabled, so nothing would ever be asked whether that call may proceed. An ' +
194
+ 'agent that can close its own tasks turns "run until the goal is met" into "run until it ' +
195
+ 'decides it is met". Enable the authorizer and give it a call policy, or do not register ' +
196
+ 'this tool.');
197
+ }
198
+ static noAgentRuntime(action) {
199
+ return new HarnessError('no_agent_runtime', `This session cannot ${action}: it was built without an agent runtime.`);
200
+ }
201
+ }
@@ -0,0 +1,67 @@
1
+ import type { JsonObject } from './json.js';
2
+ /**
3
+ * What happened during a run, for anything that wants to watch.
4
+ *
5
+ * A plain discriminated union and a synchronous listener list rather than an
6
+ * event bus: this package has no framework to hang one off, and a consumer that
7
+ * wants queues or broadcasting already has somewhere better to put them. The
8
+ * point is that the harness EMITS at the right moments, not that it owns the
9
+ * delivery mechanism.
10
+ *
11
+ * NO PROMPTS, NO TOOL ARGUMENTS, and that boundary is the same one
12
+ * `Session.completeRun()` holds: tool NAMES are what an operator needs to audit
13
+ * a guardrail and are not PII, while arguments are — `prism-opentelemetry`
14
+ * already carries those behind an opt-in capture gate with a length cap, and
15
+ * emitting them here ungated would quietly undo that decision for anyone who
16
+ * installed both.
17
+ */
18
+ export type HarnessEvent = RunStarted | RunFinished | RunFailed;
19
+ export interface RunStarted {
20
+ type: 'run.started';
21
+ runId: string;
22
+ sessionKey: string;
23
+ mode: string;
24
+ provider: string;
25
+ model: string;
26
+ /** The root of the tree this run belongs to; equal to `runId` for a root run. */
27
+ rootRunId: string;
28
+ depth: number;
29
+ at: string;
30
+ }
31
+ export interface RunFinished {
32
+ type: 'run.finished';
33
+ runId: string;
34
+ sessionKey: string;
35
+ finishReason: string;
36
+ /** NAMES only, in call order. */
37
+ toolCalls: readonly string[];
38
+ steps: number;
39
+ costUsd: number | null;
40
+ at: string;
41
+ }
42
+ export interface RunFailed {
43
+ type: 'run.failed';
44
+ runId: string;
45
+ sessionKey: string;
46
+ /** Why it stopped: a budget reason, a cancellation, or a provider failure. */
47
+ failure: string;
48
+ steps: number;
49
+ at: string;
50
+ }
51
+ export type HarnessListener = (event: HarnessEvent) => void;
52
+ export declare class HarnessEvents {
53
+ #private;
54
+ /** Returns an unsubscribe function, so a caller can stop listening. */
55
+ listen(listener: HarnessListener): () => void;
56
+ /**
57
+ * Deliver an event to every listener.
58
+ *
59
+ * A THROWING LISTENER MUST NOT BREAK THE RUN. Telemetry that takes down the
60
+ * thing it observes is worse than no telemetry, and a listener is by
61
+ * definition somebody else's code. Failures are collected and reported after
62
+ * every listener has had its turn.
63
+ */
64
+ emit(event: HarnessEvent): void;
65
+ /** For a consumer that wants to persist or forward the event as data. */
66
+ static toObject(event: HarnessEvent): JsonObject;
67
+ }
package/dist/events.js ADDED
@@ -0,0 +1,40 @@
1
+ export class HarnessEvents {
2
+ #listeners = [];
3
+ /** Returns an unsubscribe function, so a caller can stop listening. */
4
+ listen(listener) {
5
+ this.#listeners.push(listener);
6
+ return () => {
7
+ const index = this.#listeners.indexOf(listener);
8
+ if (index >= 0)
9
+ this.#listeners.splice(index, 1);
10
+ };
11
+ }
12
+ /**
13
+ * Deliver an event to every listener.
14
+ *
15
+ * A THROWING LISTENER MUST NOT BREAK THE RUN. Telemetry that takes down the
16
+ * thing it observes is worse than no telemetry, and a listener is by
17
+ * definition somebody else's code. Failures are collected and reported after
18
+ * every listener has had its turn.
19
+ */
20
+ emit(event) {
21
+ const failures = [];
22
+ for (const listener of [...this.#listeners]) {
23
+ try {
24
+ listener(event);
25
+ }
26
+ catch (error) {
27
+ failures.push(error);
28
+ }
29
+ }
30
+ if (failures.length > 0 && typeof process !== 'undefined') {
31
+ for (const failure of failures) {
32
+ process.emitWarning(`A prism-harness event listener threw while handling [${event.type}]: ${String(failure)}`);
33
+ }
34
+ }
35
+ }
36
+ /** For a consumer that wants to persist or forward the event as data. */
37
+ static toObject(event) {
38
+ return { ...event };
39
+ }
40
+ }
@@ -0,0 +1,51 @@
1
+ import type { Participant } from './session.js';
2
+ import { Session } from './session.js';
3
+ import type { SessionStore } from './stores/session-store.js';
4
+ import type { StoreManagerOptions } from './stores/store-manager.js';
5
+ import { SessionStoreManager } from './stores/store-manager.js';
6
+ export interface HarnessOptions extends Partial<StoreManagerOptions> {
7
+ /** How long ephemeral state lives. Null keeps it until forgotten. */
8
+ ttlSeconds?: number | null;
9
+ }
10
+ /**
11
+ * The entry point: `harness.for(participant).session('support')`.
12
+ *
13
+ * Mirrors the reference's `PrismHarness::for($user)->session('support')`,
14
+ * including the two-step shape — the participant is chosen first and the scope
15
+ * second, because one participant holds several unrelated conversations and the
16
+ * pair is what addresses one of them.
17
+ *
18
+ * ## The default is deliberately NOT usable for durable state
19
+ *
20
+ * With no drivers configured, both slots resolve to an in-memory store, and the
21
+ * manager then REFUSES it for the durable slot — because it reports itself
22
+ * volatile and the durable slot holds approvals a human has not answered yet.
23
+ * Constructing a harness therefore works, and asking it for durable state
24
+ * fails loudly with a message that names the fix.
25
+ *
26
+ * That is not an oversight to be smoothed over. A package that silently
27
+ * accepted an in-memory durable store would pass every test on one process and
28
+ * lose a half-executed action the first time it was deployed on two.
29
+ */
30
+ export declare class PrismHarness {
31
+ #private;
32
+ constructor(options?: HarnessOptions);
33
+ /** Bind to a participant. Returns a builder, because the scope comes next. */
34
+ for(participant: Participant): PendingSession;
35
+ ephemeralStore(): SessionStore;
36
+ durableStore(): SessionStore;
37
+ }
38
+ export declare class PendingSession {
39
+ private readonly participant;
40
+ private readonly stores;
41
+ private readonly ttlSeconds;
42
+ constructor(participant: Participant, stores: SessionStoreManager, ttlSeconds: number | null);
43
+ /**
44
+ * Resolve the session for a scope.
45
+ *
46
+ * The stores are resolved HERE, which is what makes the volatile-durable
47
+ * guard fire when a session is opened rather than at some later moment when
48
+ * an approval needs saving.
49
+ */
50
+ session(scope: string): Session;
51
+ }
@@ -0,0 +1,70 @@
1
+ import { Session } from './session.js';
2
+ import { MemorySessionStore } from './stores/memory-session-store.js';
3
+ import { SessionStoreManager } from './stores/store-manager.js';
4
+ /**
5
+ * The entry point: `harness.for(participant).session('support')`.
6
+ *
7
+ * Mirrors the reference's `PrismHarness::for($user)->session('support')`,
8
+ * including the two-step shape — the participant is chosen first and the scope
9
+ * second, because one participant holds several unrelated conversations and the
10
+ * pair is what addresses one of them.
11
+ *
12
+ * ## The default is deliberately NOT usable for durable state
13
+ *
14
+ * With no drivers configured, both slots resolve to an in-memory store, and the
15
+ * manager then REFUSES it for the durable slot — because it reports itself
16
+ * volatile and the durable slot holds approvals a human has not answered yet.
17
+ * Constructing a harness therefore works, and asking it for durable state
18
+ * fails loudly with a message that names the fix.
19
+ *
20
+ * That is not an oversight to be smoothed over. A package that silently
21
+ * accepted an in-memory durable store would pass every test on one process and
22
+ * lose a half-executed action the first time it was deployed on two.
23
+ */
24
+ export class PrismHarness {
25
+ #stores;
26
+ #ttlSeconds;
27
+ constructor(options = {}) {
28
+ this.#ttlSeconds = options.ttlSeconds ?? null;
29
+ this.#stores = new SessionStoreManager({
30
+ stores: options.stores,
31
+ drivers: options.drivers ?? { default: () => new MemorySessionStore() },
32
+ });
33
+ }
34
+ /** Bind to a participant. Returns a builder, because the scope comes next. */
35
+ for(participant) {
36
+ return new PendingSession(participant, this.#stores, this.#ttlSeconds);
37
+ }
38
+ ephemeralStore() {
39
+ return this.#stores.ephemeral();
40
+ }
41
+ durableStore() {
42
+ return this.#stores.durable();
43
+ }
44
+ }
45
+ export class PendingSession {
46
+ participant;
47
+ stores;
48
+ ttlSeconds;
49
+ constructor(participant, stores, ttlSeconds) {
50
+ this.participant = participant;
51
+ this.stores = stores;
52
+ this.ttlSeconds = ttlSeconds;
53
+ }
54
+ /**
55
+ * Resolve the session for a scope.
56
+ *
57
+ * The stores are resolved HERE, which is what makes the volatile-durable
58
+ * guard fire when a session is opened rather than at some later moment when
59
+ * an approval needs saving.
60
+ */
61
+ session(scope) {
62
+ return new Session({
63
+ participant: this.participant,
64
+ scope,
65
+ ephemeral: this.stores.ephemeral(),
66
+ durable: this.stores.durable(),
67
+ ttlSeconds: this.ttlSeconds,
68
+ });
69
+ }
70
+ }
@@ -0,0 +1,36 @@
1
+ export { HarnessError } from './errors.js';
2
+ export type { HarnessErrorCode, HarnessErrorOptions } from './errors.js';
3
+ export type { JsonObject, JsonValue } from './json.js';
4
+ export { isJsonObject } from './json.js';
5
+ export type { Durability, SessionStore } from './stores/session-store.js';
6
+ export { isDurable } from './stores/session-store.js';
7
+ export { MemorySessionStore } from './stores/memory-session-store.js';
8
+ export { FileSessionStore } from './stores/file-session-store.js';
9
+ export { SessionStoreManager, SLOT_DURABLE, SLOT_EPHEMERAL } from './stores/store-manager.js';
10
+ export type { StoreFactory, StoreManagerOptions } from './stores/store-manager.js';
11
+ export { Session } from './session.js';
12
+ export type { Participant, RunState, SessionOptions } from './session.js';
13
+ export { Thread } from './thread.js';
14
+ export type { ThreadMessage } from './thread.js';
15
+ export { PendingSession, PrismHarness } from './harness.js';
16
+ export type { HarnessOptions } from './harness.js';
17
+ export { AgentMode, ModeRegistry } from './modes.js';
18
+ export type { ModeConfig, ModeRegistryConfig } from './modes.js';
19
+ export { MAX_DEPTH, RunBudget, RunContext, RunLedger, Subagent, subagentFromConfig } from './subagents.js';
20
+ export { ToolAuthorizer, ToolRegistry, authorizedTool } from './tools.js';
21
+ export type { CallPolicy, HarnessTool, OfferPolicy, ToolAuthorizerOptions, ToolFactory, ToolProvider, } from './tools.js';
22
+ export { HarnessEvents } from './events.js';
23
+ export type { HarnessEvent, HarnessListener, RunFailed, RunFinished, RunStarted } from './events.js';
24
+ export { diagnose } from './doctor.js';
25
+ export type { DoctorFinding, DoctorReport, DoctorSubjects } from './doctor.js';
26
+ export { AgentRuntime, recordApproval } from './runtime.js';
27
+ export type { AgentResponse, AgentRuntimeOptions, LlmClient, LlmRequest, LlmResponse, LlmToolCall, PendingApproval, } from './runtime.js';
28
+ export { DEFAULT_LEASE_SECONDS, TASK_STATES, asAgentTask, attributesToRecord, canonicalTaskJson, isTaskState, isTerminalState, recordToAttributes, taskRecordFrom, withAgentTask, } from './tasks.js';
29
+ export type { AgentTask, AgentTaskAttributeSource, AgentTaskAttributes, AgentTaskRecord, AgentTaskSource, LeasedAgentTask, TaskOutcome, TaskState, } from './tasks.js';
30
+ export { StoreTaskSource, StoredAgentTask } from './store-task-source.js';
31
+ export type { Clock, StoreTaskSourceOptions } from './store-task-source.js';
32
+ export { agentCompletionTool } from './task-tools.js';
33
+ export type { CompletionToolOptions } from './task-tools.js';
34
+ export { SkillRegistry } from './skills.js';
35
+ export { SubagentRunner } from './subagent-runner.js';
36
+ export type { SubagentOutcome, SubagentResult } from './subagent-runner.js';
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ export { HarnessError } from './errors.js';
2
+ export { isJsonObject } from './json.js';
3
+ export { isDurable } from './stores/session-store.js';
4
+ export { MemorySessionStore } from './stores/memory-session-store.js';
5
+ export { FileSessionStore } from './stores/file-session-store.js';
6
+ export { SessionStoreManager, SLOT_DURABLE, SLOT_EPHEMERAL } from './stores/store-manager.js';
7
+ export { Session } from './session.js';
8
+ export { Thread } from './thread.js';
9
+ export { PendingSession, PrismHarness } from './harness.js';
10
+ export { AgentMode, ModeRegistry } from './modes.js';
11
+ export { MAX_DEPTH, RunBudget, RunContext, RunLedger, Subagent, subagentFromConfig } from './subagents.js';
12
+ export { ToolAuthorizer, ToolRegistry, authorizedTool } from './tools.js';
13
+ export { HarnessEvents } from './events.js';
14
+ export { diagnose } from './doctor.js';
15
+ export { AgentRuntime, recordApproval } from './runtime.js';
16
+ export { DEFAULT_LEASE_SECONDS, TASK_STATES, asAgentTask, attributesToRecord, canonicalTaskJson, isTaskState, isTerminalState, recordToAttributes, taskRecordFrom, withAgentTask, } from './tasks.js';
17
+ export { StoreTaskSource, StoredAgentTask } from './store-task-source.js';
18
+ export { agentCompletionTool } from './task-tools.js';
19
+ export { SkillRegistry } from './skills.js';
20
+ export { SubagentRunner } from './subagent-runner.js';
package/dist/json.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ export type JsonValue = string | number | boolean | null | JsonValue[] | JsonObject;
2
+ export interface JsonObject {
3
+ [key: string]: JsonValue | undefined;
4
+ }
5
+ export declare function isJsonObject(value: unknown): value is JsonObject;
6
+ /**
7
+ * A deep copy, by round trip.
8
+ *
9
+ * Stores hand payloads back to callers, and a caller that mutates what it was
10
+ * given would otherwise reach into the store's own state — which the in-memory
11
+ * driver makes trivially possible and every other driver makes impossible. A
12
+ * bug that only appears on one driver is the worst kind here, because the
13
+ * in-memory one is what tests run against.
14
+ */
15
+ export declare function clone<T extends JsonValue | JsonObject>(value: T): T;
package/dist/json.js ADDED
@@ -0,0 +1,15 @@
1
+ export function isJsonObject(value) {
2
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
3
+ }
4
+ /**
5
+ * A deep copy, by round trip.
6
+ *
7
+ * Stores hand payloads back to callers, and a caller that mutates what it was
8
+ * given would otherwise reach into the store's own state — which the in-memory
9
+ * driver makes trivially possible and every other driver makes impossible. A
10
+ * bug that only appears on one driver is the worst kind here, because the
11
+ * in-memory one is what tests run against.
12
+ */
13
+ export function clone(value) {
14
+ return JSON.parse(JSON.stringify(value));
15
+ }
@@ -0,0 +1,63 @@
1
+ import type { Subagent } from './subagents.js';
2
+ export interface ModeConfig {
3
+ system_prompt?: string;
4
+ tools?: readonly string[];
5
+ skills?: readonly string[];
6
+ max_steps?: number;
7
+ subagents?: Record<string, unknown>;
8
+ requires_approval?: readonly string[];
9
+ }
10
+ export interface ModeRegistryConfig {
11
+ default?: string;
12
+ modes?: Record<string, ModeConfig>;
13
+ }
14
+ /**
15
+ * What a mode lets a run do.
16
+ *
17
+ * A mode is the unit of authority here: the tools it may reach, the skills it
18
+ * loads, how many steps it gets, which nested agents it may call, and which of
19
+ * its tools need a human first.
20
+ */
21
+ export declare class AgentMode {
22
+ readonly name: string;
23
+ readonly systemPrompt: string;
24
+ readonly tools: readonly string[];
25
+ readonly skills: readonly string[];
26
+ readonly maxSteps: number;
27
+ /** Nested agents this mode may call, by name. */
28
+ readonly subagents: Readonly<Record<string, Subagent>>;
29
+ /** Tools that must not run until a human says so. */
30
+ readonly requiresApproval: readonly string[];
31
+ constructor(name: string, systemPrompt: string, tools: readonly string[], skills: readonly string[], maxSteps: number,
32
+ /** Nested agents this mode may call, by name. */
33
+ subagents?: Readonly<Record<string, Subagent>>,
34
+ /** Tools that must not run until a human says so. */
35
+ requiresApproval?: readonly string[]);
36
+ /**
37
+ * Whether a named tool needs a human before it runs IN THIS MODE.
38
+ *
39
+ * Declared per mode rather than on the tool, because the same tool is not
40
+ * equally consequential everywhere: `execute_op` against a scratch project is
41
+ * routine and against production is not, and the tool cannot tell which it is
42
+ * in. `'*'` gates every tool the mode offers.
43
+ */
44
+ needsApproval(tool: string): boolean;
45
+ }
46
+ export declare class ModeRegistry {
47
+ #private;
48
+ private readonly config;
49
+ constructor(config?: ModeRegistryConfig);
50
+ default(): string;
51
+ /** Every mode name this application has configured. */
52
+ names(): string[];
53
+ /**
54
+ * Every mode, RESOLVED.
55
+ *
56
+ * Resolving them all is the point: `resolve()` validates as it goes, so a
57
+ * mode nobody has entered yet keeps its misconfiguration until the day
58
+ * somebody switches to it. This is what the doctor uses to find that on a
59
+ * Tuesday rather than in front of a user.
60
+ */
61
+ all(): Record<string, AgentMode>;
62
+ resolve(name?: string | null): AgentMode;
63
+ }