@particle-academy/prism-harness 0.8.0 → 0.9.1
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/README.md +85 -15
- package/dist/address-index.d.ts +35 -0
- package/dist/address-index.js +170 -0
- package/dist/address.d.ts +25 -0
- package/dist/address.js +83 -0
- package/dist/harness.d.ts +41 -2
- package/dist/harness.js +99 -4
- package/dist/index.d.ts +11 -5
- package/dist/index.js +4 -1
- package/dist/modes.d.ts +10 -1
- package/dist/modes.js +53 -1
- package/dist/registration.d.ts +6 -0
- package/dist/registration.js +15 -0
- package/dist/runtime.d.ts +21 -15
- package/dist/runtime.js +85 -7
- package/dist/session.d.ts +12 -0
- package/dist/session.js +66 -11
- package/dist/stores/file-lock-identity.d.ts +20 -0
- package/dist/stores/file-lock-identity.js +74 -0
- package/dist/stores/file-lock-windows-identity.d.ts +2 -0
- package/dist/stores/file-lock-windows-identity.js +48 -0
- package/dist/stores/file-owner-lock.d.ts +28 -0
- package/dist/stores/file-owner-lock.js +280 -0
- package/dist/stores/file-session-store.d.ts +13 -6
- package/dist/stores/file-session-store.js +16 -183
- package/dist/stores/memory-session-store.d.ts +2 -0
- package/dist/stores/memory-session-store.js +2 -0
- package/dist/stores/session-store.d.ts +4 -0
- package/dist/subagent-runner.js +10 -3
- package/dist/subagents.d.ts +2 -1
- package/dist/subagents.js +10 -3
- package/dist/task-tools.js +6 -1
- package/dist/thread.d.ts +2 -0
- package/dist/thread.js +6 -0
- package/dist/tools.d.ts +28 -5
- package/dist/tools.js +159 -21
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -2,23 +2,28 @@ export { HarnessError } from './errors.js';
|
|
|
2
2
|
export type { HarnessErrorCode, HarnessErrorOptions } from './errors.js';
|
|
3
3
|
export type { JsonObject, JsonValue } from './json.js';
|
|
4
4
|
export { isJsonObject } from './json.js';
|
|
5
|
-
export type { AppendLogStore, Durability, SessionStore } from './stores/session-store.js';
|
|
5
|
+
export type { AppendLogStore, Durability, LockScope, SessionStore } from './stores/session-store.js';
|
|
6
6
|
export { isDurable, supportsAppendLog } from './stores/session-store.js';
|
|
7
7
|
export { MemorySessionStore } from './stores/memory-session-store.js';
|
|
8
8
|
export { FileSessionStore } from './stores/file-session-store.js';
|
|
9
|
+
export type { PendingFileLockRelease } from './stores/file-owner-lock.js';
|
|
10
|
+
export type { FileLockIdentityStatus } from './stores/file-lock-identity.js';
|
|
9
11
|
export { SessionStoreManager, SLOT_DURABLE, SLOT_EPHEMERAL } from './stores/store-manager.js';
|
|
10
12
|
export type { StoreFactory, StoreManagerOptions } from './stores/store-manager.js';
|
|
11
13
|
export { Session } from './session.js';
|
|
12
14
|
export type { Participant, RunState, SessionOptions } from './session.js';
|
|
13
15
|
export { Thread } from './thread.js';
|
|
14
16
|
export type { ThreadMessage } from './thread.js';
|
|
15
|
-
export { PendingSession, PrismHarness } from './harness.js';
|
|
16
|
-
export
|
|
17
|
+
export { AgentHarness, PendingSession, PrismHarness } from './harness.js';
|
|
18
|
+
export { ParticipantAdapters, ParticipantRef, SessionAddress } from './address.js';
|
|
19
|
+
export { AddressLimitExceeded, IndexLockScopeUnsupported, IndexRecoveryRequired, StoreAddressIndex } from './address-index.js';
|
|
20
|
+
export type { AddressIndexPage } from './address-index.js';
|
|
21
|
+
export type { AgentHarnessConfig, HarnessOptions } from './harness.js';
|
|
17
22
|
export { AgentMode, ModeRegistry } from './modes.js';
|
|
18
23
|
export type { ModeConfig, ModeRegistryConfig } from './modes.js';
|
|
19
24
|
export { MAX_DEPTH, RunBudget, RunContext, RunLedger, Subagent, subagentFromConfig } from './subagents.js';
|
|
20
25
|
export { ToolAuthorizer, ToolRegistry, authorizedTool } from './tools.js';
|
|
21
|
-
export type { CallPolicy, HarnessTool, OfferPolicy, ToolAuthorizerOptions, ToolFactory, ToolProvider, } from './tools.js';
|
|
26
|
+
export type { CallPolicy, HarnessTool, OfferPolicy, ToolAuthorizerOptions, ToolFactory, RegistryOptions, ToolProvider, } from './tools.js';
|
|
22
27
|
export { HarnessEvents } from './events.js';
|
|
23
28
|
export type { HarnessEvent, HarnessListener, RunFailed, RunFinished, RunStarted } from './events.js';
|
|
24
29
|
export { diagnose } from './doctor.js';
|
|
@@ -26,7 +31,7 @@ export type { DoctorFinding, DoctorReport, DoctorSubjects } from './doctor.js';
|
|
|
26
31
|
export { AgentRuntime, recordApproval } from './runtime.js';
|
|
27
32
|
export { assistantRow, threadView, toolResultEntry, toolResultRow } from './thread-rows.js';
|
|
28
33
|
export type { ApprovalDecisionEntry, ApprovalRequestEntry, AssistantRow, ToolCallInput, ToolCallRow, ToolResultEntry, ToolResultRow, } from './thread-rows.js';
|
|
29
|
-
export type { AgentResponse, AgentRuntimeOptions, LlmClient, LlmRequest, LlmResponse, LlmToolCall, PendingApproval, StructuredAgentResponse, } from './runtime.js';
|
|
34
|
+
export type { AgentResponse, AgentRuntimeOptions, EffectiveTool, RuntimeConfig, LlmClient, LlmRequest, LlmResponse, LlmToolCall, PendingApproval, StructuredAgentResponse, } from './runtime.js';
|
|
30
35
|
export { schemaName, schemaProblems } from './structured.js';
|
|
31
36
|
export { DEFAULT_LEASE_SECONDS, TASK_STATES, asAgentTask, attributesToRecord, canonicalTaskJson, isTaskState, isTerminalState, recordToAttributes, taskRecordFrom, withAgentTask, } from './tasks.js';
|
|
32
37
|
export type { AgentTask, AgentTaskAttributeSource, AgentTaskAttributes, AgentTaskRecord, AgentTaskSource, LeasedAgentTask, TaskOutcome, TaskState, } from './tasks.js';
|
|
@@ -38,4 +43,5 @@ export { SkillRegistry } from './skills.js';
|
|
|
38
43
|
export { SubagentRunner } from './subagent-runner.js';
|
|
39
44
|
export type { SubagentOutcome, SubagentResult } from './subagent-runner.js';
|
|
40
45
|
export { admitAttachments } from './attachments.js';
|
|
46
|
+
export { RegistrationCollision } from './registration.js';
|
|
41
47
|
export type { Attachment, MediaLike } from './attachments.js';
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,9 @@ export { FileSessionStore } from './stores/file-session-store.js';
|
|
|
6
6
|
export { SessionStoreManager, SLOT_DURABLE, SLOT_EPHEMERAL } from './stores/store-manager.js';
|
|
7
7
|
export { Session } from './session.js';
|
|
8
8
|
export { Thread } from './thread.js';
|
|
9
|
-
export { PendingSession, PrismHarness } from './harness.js';
|
|
9
|
+
export { AgentHarness, PendingSession, PrismHarness } from './harness.js';
|
|
10
|
+
export { ParticipantAdapters, ParticipantRef, SessionAddress } from './address.js';
|
|
11
|
+
export { AddressLimitExceeded, IndexLockScopeUnsupported, IndexRecoveryRequired, StoreAddressIndex } from './address-index.js';
|
|
10
12
|
export { AgentMode, ModeRegistry } from './modes.js';
|
|
11
13
|
export { MAX_DEPTH, RunBudget, RunContext, RunLedger, Subagent, subagentFromConfig } from './subagents.js';
|
|
12
14
|
export { ToolAuthorizer, ToolRegistry, authorizedTool } from './tools.js';
|
|
@@ -21,3 +23,4 @@ export { agentCompletionTool } from './task-tools.js';
|
|
|
21
23
|
export { SkillRegistry } from './skills.js';
|
|
22
24
|
export { SubagentRunner } from './subagent-runner.js';
|
|
23
25
|
export { admitAttachments } from './attachments.js';
|
|
26
|
+
export { RegistrationCollision } from './registration.js';
|
package/dist/modes.d.ts
CHANGED
|
@@ -11,6 +11,9 @@ export interface ModeConfig {
|
|
|
11
11
|
provider_options?: Record<string, unknown>;
|
|
12
12
|
}
|
|
13
13
|
export interface ModeRegistryConfig {
|
|
14
|
+
refuseDuplicates?: boolean;
|
|
15
|
+
owner?: string;
|
|
16
|
+
agents?: Record<string, ModeRegistryConfig>;
|
|
14
17
|
default?: string;
|
|
15
18
|
modes?: Record<string, ModeConfig>;
|
|
16
19
|
}
|
|
@@ -53,7 +56,13 @@ export declare class AgentMode {
|
|
|
53
56
|
export declare class ModeRegistry {
|
|
54
57
|
#private;
|
|
55
58
|
private readonly config;
|
|
56
|
-
|
|
59
|
+
private readonly agent;
|
|
60
|
+
private readonly root?;
|
|
61
|
+
constructor(config?: ModeRegistryConfig, agent?: string | null, root?: ModeRegistry | undefined);
|
|
62
|
+
forAgent(agent: string | null): ModeRegistry;
|
|
63
|
+
register(name: string, mode: ModeConfig, owner?: string): this;
|
|
64
|
+
replace(name: string, mode: ModeConfig, predecessorOwner: string, owner?: string): this;
|
|
65
|
+
setDefault(name: string): this;
|
|
57
66
|
default(): string;
|
|
58
67
|
/** Every mode name this application has configured. */
|
|
59
68
|
names(): string[];
|
package/dist/modes.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { HarnessError } from './errors.js';
|
|
2
2
|
import { isJsonObject } from './json.js';
|
|
3
3
|
import { subagentFromConfig } from './subagents.js';
|
|
4
|
+
import { validateAgent } from './address.js';
|
|
5
|
+
import { RegistrationCollision, registrationOwner } from './registration.js';
|
|
4
6
|
/**
|
|
5
7
|
* What a mode lets a run do.
|
|
6
8
|
*
|
|
@@ -47,8 +49,58 @@ export class AgentMode {
|
|
|
47
49
|
}
|
|
48
50
|
export class ModeRegistry {
|
|
49
51
|
config;
|
|
50
|
-
|
|
52
|
+
agent;
|
|
53
|
+
root;
|
|
54
|
+
#agents = new Map();
|
|
55
|
+
#owners = new Map();
|
|
56
|
+
constructor(config = {}, agent = null, root) {
|
|
51
57
|
this.config = config;
|
|
58
|
+
this.agent = agent;
|
|
59
|
+
this.root = root;
|
|
60
|
+
if (config.refuseDuplicates !== undefined && typeof config.refuseDuplicates !== 'boolean')
|
|
61
|
+
throw new TypeError('refuseDuplicates must be a boolean');
|
|
62
|
+
validateAgent(agent);
|
|
63
|
+
for (const name of Object.keys(config.modes ?? {}))
|
|
64
|
+
this.#owners.set(name, registrationOwner(config.owner, agent));
|
|
65
|
+
for (const key of ['config', 'agent', 'root'])
|
|
66
|
+
Object.defineProperty(this, key, { writable: false, configurable: false });
|
|
67
|
+
}
|
|
68
|
+
forAgent(agent) {
|
|
69
|
+
validateAgent(agent);
|
|
70
|
+
const root = this.root ?? this;
|
|
71
|
+
if (agent === null)
|
|
72
|
+
return root;
|
|
73
|
+
let view = root.#agents.get(agent);
|
|
74
|
+
if (!view) {
|
|
75
|
+
const configured = root.config.agents?.[agent] ?? {};
|
|
76
|
+
view = new ModeRegistry({ ...structuredClone(configured), refuseDuplicates: root.config.refuseDuplicates, default: configured.default ?? root.default() }, agent, root);
|
|
77
|
+
root.#agents.set(agent, view);
|
|
78
|
+
}
|
|
79
|
+
return view;
|
|
80
|
+
}
|
|
81
|
+
register(name, mode, owner) {
|
|
82
|
+
const label = registrationOwner(owner, this.agent), previous = this.#owners.get(name);
|
|
83
|
+
if ((this.agent !== null || this.config.refuseDuplicates === true) && previous !== undefined)
|
|
84
|
+
throw new RegistrationCollision(this.agent, name, previous, label);
|
|
85
|
+
this.#set(name, mode, label);
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
replace(name, mode, predecessorOwner, owner) {
|
|
89
|
+
const label = registrationOwner(owner, this.agent);
|
|
90
|
+
if (this.#owners.get(name) !== predecessorOwner)
|
|
91
|
+
throw new Error('Replacement must name the registered predecessor owner.');
|
|
92
|
+
this.#set(name, mode, label);
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
#set(name, mode, owner) {
|
|
96
|
+
this.config.modes ??= {};
|
|
97
|
+
this.config.modes[name] = structuredClone(mode);
|
|
98
|
+
this.#owners.set(name, owner);
|
|
99
|
+
}
|
|
100
|
+
setDefault(name) {
|
|
101
|
+
this.resolve(name);
|
|
102
|
+
this.config.default = name;
|
|
103
|
+
return this;
|
|
52
104
|
}
|
|
53
105
|
default() {
|
|
54
106
|
const configured = this.config.default;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare class RegistrationCollision extends Error {
|
|
2
|
+
readonly code = "registration_collision";
|
|
3
|
+
constructor(agent: string | null, name: string, previous: string, incoming: string);
|
|
4
|
+
}
|
|
5
|
+
/** Diagnostic labels are host configuration, never permission grants. */
|
|
6
|
+
export declare function registrationOwner(owner: string | undefined, agent: string | null): string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class RegistrationCollision extends Error {
|
|
2
|
+
code = 'registration_collision';
|
|
3
|
+
constructor(agent, name, previous, incoming) {
|
|
4
|
+
super(`Harness registry collision: agent [${agent ?? 'legacy'}], name [${name}], owners [${previous}] and [${incoming}]. Use replace naming predecessor owner [${previous}].`);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
/** Diagnostic labels are host configuration, never permission grants. */
|
|
8
|
+
export function registrationOwner(owner, agent) {
|
|
9
|
+
const label = owner ?? (agent === null ? 'legacy' : undefined);
|
|
10
|
+
if (typeof label !== 'string' || label.length === 0 || Buffer.byteLength(label) > 128
|
|
11
|
+
|| Buffer.from(label, 'utf8').toString('utf8') !== label || /[\u0000-\u001f\u007f]/.test(label)) {
|
|
12
|
+
throw new TypeError('A named registration requires a non-empty UTF-8 owner label of at most 128 bytes without control characters.');
|
|
13
|
+
}
|
|
14
|
+
return label;
|
|
15
|
+
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ import type { HarnessTool, ToolAuthorizer, ToolRegistry } from './tools.js';
|
|
|
16
16
|
* The reference couples these because Prism is already a dependency there.
|
|
17
17
|
*/
|
|
18
18
|
export interface LlmRequest {
|
|
19
|
+
/** Optional ceiling the model client must pass to its provider request. */
|
|
20
|
+
maxOutputTokens?: number;
|
|
19
21
|
systemPrompt: string;
|
|
20
22
|
/** The conversation so far, serialized — oldest first. */
|
|
21
23
|
messages: readonly JsonObject[];
|
|
@@ -108,15 +110,34 @@ export interface PendingApproval {
|
|
|
108
110
|
arguments: JsonObject;
|
|
109
111
|
}
|
|
110
112
|
export interface AgentRuntimeOptions {
|
|
113
|
+
/** Legacy compatibility default. Named agents always enforce their mode ceiling. */
|
|
114
|
+
narrowRequestedTools?: boolean;
|
|
115
|
+
config?: RuntimeConfig;
|
|
116
|
+
agents?: Record<string, RuntimeConfig>;
|
|
111
117
|
client: LlmClient;
|
|
112
118
|
modes: ModeRegistry;
|
|
113
119
|
tools: ToolRegistry;
|
|
114
120
|
authorizer?: ToolAuthorizer;
|
|
115
121
|
events?: HarnessEvents;
|
|
116
122
|
}
|
|
123
|
+
export interface RuntimeConfig {
|
|
124
|
+
provider?: string | null;
|
|
125
|
+
model?: string | null;
|
|
126
|
+
max_steps_ceiling?: number | null;
|
|
127
|
+
max_output_tokens_per_call?: number | null;
|
|
128
|
+
lock_ttl?: number;
|
|
129
|
+
lock_wait?: number;
|
|
130
|
+
}
|
|
131
|
+
export interface EffectiveTool {
|
|
132
|
+
name: string;
|
|
133
|
+
owner: string;
|
|
134
|
+
source: string;
|
|
135
|
+
}
|
|
117
136
|
export declare class AgentRuntime {
|
|
118
137
|
#private;
|
|
119
138
|
constructor(options: AgentRuntimeOptions);
|
|
139
|
+
/** Inspect the offer without a run/handler or logical mutation; reads may purge expired entries. */
|
|
140
|
+
effectiveTools(session: Session, modeName?: string): Promise<EffectiveTool[]>;
|
|
120
141
|
/**
|
|
121
142
|
* Run a turn.
|
|
122
143
|
*
|
|
@@ -149,19 +170,4 @@ export declare class AgentRuntime {
|
|
|
149
170
|
*/
|
|
150
171
|
sendStructured(session: Session, prompt: string, schema: Readonly<JsonObject>, toolNames?: readonly string[], context?: RunContext, additionalContent?: readonly unknown[]): Promise<StructuredAgentResponse>;
|
|
151
172
|
}
|
|
152
|
-
/**
|
|
153
|
-
* Answer a pending approval, durably.
|
|
154
|
-
*
|
|
155
|
-
* The decision is RECORDED IN THE THREAD, not held anywhere else. Who may
|
|
156
|
-
* approve is the APPLICATION's decision, not this package's: the session is
|
|
157
|
-
* already scoped to a participant, so nobody can answer another participant's
|
|
158
|
-
* approval through it, but "this user may approve THIS action" is a question
|
|
159
|
-
* only the host can answer. Authorize before calling.
|
|
160
|
-
*
|
|
161
|
-
* Nothing runs until the next `send()`, which acts on every decision recorded
|
|
162
|
-
* by then and refuses any pending call still without one. With several pending,
|
|
163
|
-
* record them all first.
|
|
164
|
-
*
|
|
165
|
-
* `approvalId` is `PendingApproval.id`, not the tool call id.
|
|
166
|
-
*/
|
|
167
173
|
export declare function recordApproval(session: Session, approvalId: string, approved: boolean, reason?: string | null): Promise<void>;
|
package/dist/runtime.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { validateAgent } from './address.js';
|
|
2
3
|
import { assistantRow, threadView, toolResultEntry, toolResultRow, } from './thread-rows.js';
|
|
3
4
|
import { admitAttachments } from './attachments.js';
|
|
4
5
|
import { HarnessError } from './errors.js';
|
|
@@ -32,12 +33,56 @@ export class AgentRuntime {
|
|
|
32
33
|
#tools;
|
|
33
34
|
#authorizer;
|
|
34
35
|
#events;
|
|
36
|
+
#config;
|
|
37
|
+
#agents;
|
|
38
|
+
#narrowRequestedTools;
|
|
35
39
|
constructor(options) {
|
|
40
|
+
if (options.narrowRequestedTools !== undefined && typeof options.narrowRequestedTools !== 'boolean')
|
|
41
|
+
throw new TypeError('narrowRequestedTools must be a boolean');
|
|
42
|
+
this.#narrowRequestedTools = options.narrowRequestedTools ?? false;
|
|
36
43
|
this.#client = options.client;
|
|
37
44
|
this.#modes = options.modes;
|
|
38
45
|
this.#tools = options.tools;
|
|
39
46
|
this.#authorizer = options.authorizer;
|
|
40
47
|
this.#events = options.events;
|
|
48
|
+
this.#config = { ...options.config };
|
|
49
|
+
this.#agents = Object.fromEntries(Object.entries(options.agents ?? {}).map(([agent, config]) => {
|
|
50
|
+
validateAgent(agent);
|
|
51
|
+
validateRuntimeConfig(config);
|
|
52
|
+
return [agent, { ...config }];
|
|
53
|
+
}));
|
|
54
|
+
validateRuntimeConfig(this.#config);
|
|
55
|
+
}
|
|
56
|
+
#setting(session, key) {
|
|
57
|
+
const options = session.agent === null ? undefined : this.#agents[session.agent];
|
|
58
|
+
return options && Object.hasOwn(options, key) ? options[key] : this.#config[key];
|
|
59
|
+
}
|
|
60
|
+
#boundedRun(session, run) {
|
|
61
|
+
const steps = this.#setting(session, 'max_steps_ceiling'), output = this.#setting(session, 'max_output_tokens_per_call'), budget = run.budget;
|
|
62
|
+
if (steps == null && output == null)
|
|
63
|
+
return run;
|
|
64
|
+
return new RunContext(run.ledger, new RunBudget(steps == null ? budget.maxSteps : Math.min(budget.maxSteps, run.ledger.steps + steps), budget.maxCostUsd, budget.maxSeconds, output == null ? budget.maxOutputTokensPerCall : Math.min(budget.maxOutputTokensPerCall ?? output, output)), run.parentRunId, run.depth);
|
|
65
|
+
}
|
|
66
|
+
/** Inspect the offer without a run/handler or logical mutation; reads may purge expired entries. */
|
|
67
|
+
async effectiveTools(session, modeName) {
|
|
68
|
+
const mode = this.#modes.forAgent(session.agent).resolve(modeName ?? await session.mode());
|
|
69
|
+
return (await this.#offer(session, mode)).descriptors;
|
|
70
|
+
}
|
|
71
|
+
async #offer(session, mode, requested) {
|
|
72
|
+
return session.resolveReadOnly(mode.name, async (view) => {
|
|
73
|
+
const names = session.agent !== null || this.#narrowRequestedTools ? narrowedTools(mode.tools, requested) : (requested ?? mode.tools);
|
|
74
|
+
const detailed = await this.#tools.resolveDetailed(names, view);
|
|
75
|
+
const tools = new Map([...detailed].map(([name, entry]) => [name, entry.tool]));
|
|
76
|
+
if (view.agent !== null && tools.has('complete_task')) {
|
|
77
|
+
const authorizer = this.#authorizer?.forAgent(view.agent);
|
|
78
|
+
if (!authorizer?.enabled || !authorizer.hasCallPolicy())
|
|
79
|
+
throw new HarnessError('unsafe_authorization_configuration', 'Named task completion requires enabled authorization and an explicit call policy.');
|
|
80
|
+
}
|
|
81
|
+
const offered = this.#authorizer ? await this.#authorizer.allowed(view, tools) : [...tools.values()];
|
|
82
|
+
const ownership = new Map([...detailed.values()].map(entry => [entry.tool.name, entry]));
|
|
83
|
+
const descriptors = offered.map(tool => ({ name: tool.name, owner: ownership.get(tool.name).owner, source: ownership.get(tool.name).source }));
|
|
84
|
+
return { tools: offered, descriptors };
|
|
85
|
+
});
|
|
41
86
|
}
|
|
42
87
|
/**
|
|
43
88
|
* Run a turn.
|
|
@@ -80,11 +125,11 @@ export class AgentRuntime {
|
|
|
80
125
|
// Refused before a run exists: a bad attachment is a mistake in the call,
|
|
81
126
|
// and it should not cost a run, events or budget.
|
|
82
127
|
const attachments = admitAttachments(prompt, additionalContent);
|
|
83
|
-
const mode = this.#modes.resolve(await session.mode());
|
|
84
|
-
const provider = (await session.provider()) ?? 'unknown';
|
|
85
|
-
const model = (await session.model()) ?? 'unknown';
|
|
128
|
+
const mode = this.#modes.forAgent(session.agent).resolve(await session.mode());
|
|
129
|
+
const provider = (await session.provider()) ?? this.#setting(session, 'provider') ?? 'unknown';
|
|
130
|
+
const model = (await session.model()) ?? this.#setting(session, 'model') ?? 'unknown';
|
|
86
131
|
const runId = randomUUID();
|
|
87
|
-
const run = context ?? RunContext.root(runId, new RunBudget(mode.maxSteps));
|
|
132
|
+
const run = this.#boundedRun(session, context ?? RunContext.root(runId, new RunBudget(mode.maxSteps)));
|
|
88
133
|
const thread = session.thread();
|
|
89
134
|
if (run.tooDeep()) {
|
|
90
135
|
throw HarnessError.runNotPermitted(`This run is nested ${run.depth} deep, at or past the ceiling. Nobody debugs a tree that deep, ` +
|
|
@@ -103,8 +148,13 @@ export class AgentRuntime {
|
|
|
103
148
|
at: new Date().toISOString(),
|
|
104
149
|
});
|
|
105
150
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
151
|
+
let offered;
|
|
152
|
+
if (session.agent === null && !this.#narrowRequestedTools) {
|
|
153
|
+
const resolved = await this.#tools.resolve(toolNames ?? mode.tools, session);
|
|
154
|
+
offered = this.#authorizer ? await this.#authorizer.allowed(session, resolved) : [...resolved.values()];
|
|
155
|
+
}
|
|
156
|
+
else
|
|
157
|
+
offered = (await this.#offer(session, mode, toolNames)).tools;
|
|
108
158
|
const called = [];
|
|
109
159
|
// Decisions recorded since the run stopped are acted on FIRST, before a
|
|
110
160
|
// new prompt is recorded, so the results land after the calls they answer
|
|
@@ -164,6 +214,7 @@ export class AgentRuntime {
|
|
|
164
214
|
provider,
|
|
165
215
|
model,
|
|
166
216
|
providerOptions: mode.providerOptions,
|
|
217
|
+
...(run.budget.maxOutputTokensPerCall === null ? {} : { maxOutputTokens: run.budget.maxOutputTokensPerCall }),
|
|
167
218
|
...(schema === undefined ? {} : { schema }),
|
|
168
219
|
});
|
|
169
220
|
run.ledger.recordSteps(1);
|
|
@@ -279,7 +330,7 @@ export class AgentRuntime {
|
|
|
279
330
|
}
|
|
280
331
|
}
|
|
281
332
|
await live.thread().record([toolResultRow([...work.results.values(), ...resolved], [...work.decisions.values()])], runId);
|
|
282
|
-
}, RESOLUTION_LOCK_SECONDS, RESOLUTION_WAIT_SECONDS);
|
|
333
|
+
}, this.#setting(session, 'lock_ttl') ?? RESOLUTION_LOCK_SECONDS, this.#setting(session, 'lock_wait') ?? RESOLUTION_WAIT_SECONDS);
|
|
283
334
|
}
|
|
284
335
|
/**
|
|
285
336
|
* The gated calls of the last tool-calling turn that have no result yet, with
|
|
@@ -398,6 +449,33 @@ export class AgentRuntime {
|
|
|
398
449
|
*
|
|
399
450
|
* `approvalId` is `PendingApproval.id`, not the tool call id.
|
|
400
451
|
*/
|
|
452
|
+
function validateRuntimeConfig(config) {
|
|
453
|
+
for (const key of ['provider', 'model']) {
|
|
454
|
+
const value = config[key];
|
|
455
|
+
if (value !== undefined && value !== null && (typeof value !== 'string' || value === ''))
|
|
456
|
+
throw new TypeError('Agent provider/model defaults must be nonempty strings or null.');
|
|
457
|
+
}
|
|
458
|
+
for (const key of ['max_steps_ceiling', 'max_output_tokens_per_call']) {
|
|
459
|
+
const value = config[key];
|
|
460
|
+
if (value !== undefined && value !== null && (!Number.isSafeInteger(value) || value < 1))
|
|
461
|
+
throw new TypeError('Agent step/output ceilings must be positive integers or null.');
|
|
462
|
+
}
|
|
463
|
+
for (const key of ['lock_ttl', 'lock_wait']) {
|
|
464
|
+
const value = config[key];
|
|
465
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < (key === 'lock_ttl' ? 1 : 0)))
|
|
466
|
+
throw new TypeError('Agent lock settings must be whole seconds with positive TTL and nonnegative wait.');
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function narrowedTools(ceiling, requested) {
|
|
470
|
+
if (requested === undefined || requested.includes('*'))
|
|
471
|
+
return ceiling;
|
|
472
|
+
if (ceiling.includes('*'))
|
|
473
|
+
return requested;
|
|
474
|
+
const refused = requested.find(name => !ceiling.includes(name));
|
|
475
|
+
if (refused !== undefined)
|
|
476
|
+
throw HarnessError.toolNotAvailable(refused, ceiling);
|
|
477
|
+
return requested;
|
|
478
|
+
}
|
|
401
479
|
export async function recordApproval(session, approvalId, approved, reason = null) {
|
|
402
480
|
const run = await session.run();
|
|
403
481
|
await session.thread().record([toolResultRow([], [{ approval_id: approvalId, approved, reason }])], run?.id ?? null);
|
package/dist/session.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SessionAddress } from './address.js';
|
|
2
|
+
import { StoreAddressIndex } from './address-index.js';
|
|
1
3
|
import type { JsonObject } from './json.js';
|
|
2
4
|
import type { SessionStore } from './stores/session-store.js';
|
|
3
5
|
import { StoreTaskSource } from './store-task-source.js';
|
|
@@ -29,6 +31,10 @@ export interface SessionOptions {
|
|
|
29
31
|
durable: SessionStore;
|
|
30
32
|
/** How long ephemeral state lives. Null keeps it until forgotten. */
|
|
31
33
|
ttlSeconds?: number | null;
|
|
34
|
+
agent?: string | null;
|
|
35
|
+
actor?: object;
|
|
36
|
+
addressIndex?: StoreAddressIndex;
|
|
37
|
+
maxAddressesPerParticipant?: number | null;
|
|
32
38
|
}
|
|
33
39
|
/**
|
|
34
40
|
* One participant's live runtime, reconstructed per turn.
|
|
@@ -47,6 +53,8 @@ export interface SessionOptions {
|
|
|
47
53
|
*/
|
|
48
54
|
export declare class Session {
|
|
49
55
|
#private;
|
|
56
|
+
readonly address: SessionAddress;
|
|
57
|
+
readonly actor: object;
|
|
50
58
|
readonly scope: string;
|
|
51
59
|
readonly participant: Participant;
|
|
52
60
|
constructor(options: SessionOptions);
|
|
@@ -64,6 +72,7 @@ export declare class Session {
|
|
|
64
72
|
* ONE store and resolve the same session.
|
|
65
73
|
*/
|
|
66
74
|
key(): string;
|
|
75
|
+
get agent(): string | null;
|
|
67
76
|
mode(): Promise<string | null>;
|
|
68
77
|
usingMode(mode: string): Promise<this>;
|
|
69
78
|
model(): Promise<string | null>;
|
|
@@ -75,6 +84,7 @@ export declare class Session {
|
|
|
75
84
|
forget(): Promise<this>;
|
|
76
85
|
/** The stored conversation this session is bound to. */
|
|
77
86
|
thread(): Thread;
|
|
87
|
+
findThread(): Promise<Thread | null>;
|
|
78
88
|
/**
|
|
79
89
|
* The agent task list this session is bound to.
|
|
80
90
|
*
|
|
@@ -111,4 +121,6 @@ export declare class Session {
|
|
|
111
121
|
* run inside this, not outside it.
|
|
112
122
|
*/
|
|
113
123
|
lock<T>(callback: (session: this) => T | Promise<T>, ttlSeconds?: number, waitSeconds?: number): Promise<T>;
|
|
124
|
+
/** Refuse explicit logical mutations; ordinary get() retains expired-entry cleanup. */
|
|
125
|
+
resolveReadOnly<T>(mode: string, resolve: (session: Session) => T | Promise<T>): Promise<T>;
|
|
114
126
|
}
|
package/dist/session.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ParticipantRef, SessionAddress } from './address.js';
|
|
2
|
+
import { indexedStore, storeLockRank, StoreAddressIndex } from './address-index.js';
|
|
2
3
|
import { isJsonObject } from './json.js';
|
|
3
4
|
import { StoreTaskSource } from './store-task-source.js';
|
|
4
5
|
import { Thread } from './thread.js';
|
|
@@ -18,18 +19,39 @@ import { Thread } from './thread.js';
|
|
|
18
19
|
* - the thread and stored capabilities are DURABLE.
|
|
19
20
|
*/
|
|
20
21
|
export class Session {
|
|
22
|
+
address;
|
|
23
|
+
actor;
|
|
24
|
+
#addressIndex;
|
|
25
|
+
#rawDurable;
|
|
26
|
+
#rawEphemeral;
|
|
27
|
+
#lockStore;
|
|
21
28
|
scope;
|
|
22
29
|
participant;
|
|
23
30
|
#ephemeral;
|
|
24
31
|
#durable;
|
|
25
32
|
#ttlSeconds;
|
|
26
33
|
#cachedState = null;
|
|
34
|
+
#resolutionOnly = false;
|
|
35
|
+
#resolvedMode = null;
|
|
27
36
|
constructor(options) {
|
|
28
|
-
|
|
37
|
+
if (options.participant instanceof ParticipantRef && options.actor === undefined)
|
|
38
|
+
throw new Error('Typed participants require an explicit authorization actor.');
|
|
39
|
+
const strictIdentity = options.agent != null || options.participant instanceof ParticipantRef;
|
|
40
|
+
this.participant = options.participant instanceof ParticipantRef ? options.participant : (strictIdentity ? Object.freeze({ ...options.participant }) : options.participant);
|
|
41
|
+
this.address = new SessionAddress(this.participant, options.scope, options.agent ?? null);
|
|
42
|
+
this.actor = options.actor ?? this.participant;
|
|
43
|
+
const index = options.addressIndex ?? (this.address.agent !== null || options.participant instanceof ParticipantRef
|
|
44
|
+
? new StoreAddressIndex(options.durable, options.maxAddressesPerParticipant ?? null) : undefined);
|
|
45
|
+
this.#addressIndex = index;
|
|
46
|
+
this.#rawDurable = options.durable;
|
|
47
|
+
this.#rawEphemeral = options.ephemeral;
|
|
29
48
|
this.scope = options.scope;
|
|
30
|
-
this.#ephemeral = options.ephemeral;
|
|
31
|
-
this.#durable = options.durable;
|
|
49
|
+
this.#ephemeral = index ? indexedStore(options.ephemeral, index, this.address, 'ephemeral') : options.ephemeral;
|
|
50
|
+
this.#durable = index ? indexedStore(options.durable, index, this.address, 'durable') : options.durable;
|
|
51
|
+
this.#lockStore = index && storeLockRank(options.durable) > storeLockRank(options.ephemeral) ? this.#durable : this.#ephemeral;
|
|
32
52
|
this.#ttlSeconds = options.ttlSeconds ?? null;
|
|
53
|
+
for (const key of strictIdentity ? ['address', 'actor', 'participant', 'scope'] : ['address', 'actor'])
|
|
54
|
+
Object.defineProperty(this, key, { writable: false, configurable: false });
|
|
33
55
|
}
|
|
34
56
|
/**
|
|
35
57
|
* The address this session is resolved by.
|
|
@@ -45,11 +67,14 @@ export class Session {
|
|
|
45
67
|
* ONE store and resolve the same session.
|
|
46
68
|
*/
|
|
47
69
|
key() {
|
|
48
|
-
|
|
49
|
-
|
|
70
|
+
return this.address.agent === null && !(this.participant instanceof ParticipantRef)
|
|
71
|
+
? new SessionAddress(this.participant, this.scope).sessionKey() : this.address.sessionKey();
|
|
50
72
|
}
|
|
73
|
+
get agent() { return this.address.agent; }
|
|
51
74
|
// -- ephemeral half --------------------------------------------------------
|
|
52
75
|
async mode() {
|
|
76
|
+
if (this.#resolvedMode !== null)
|
|
77
|
+
return this.#resolvedMode;
|
|
53
78
|
return this.#readString('mode');
|
|
54
79
|
}
|
|
55
80
|
async usingMode(mode) {
|
|
@@ -69,10 +94,11 @@ export class Session {
|
|
|
69
94
|
}
|
|
70
95
|
async state() {
|
|
71
96
|
this.#cachedState ??= (await this.#ephemeral.get(this.#ephemeralKey())) ?? {};
|
|
72
|
-
return this.#cachedState;
|
|
97
|
+
return this.#resolutionOnly ? structuredClone(this.#cachedState) : this.#cachedState;
|
|
73
98
|
}
|
|
74
99
|
/** Drop the ephemeral half. THE CONVERSATION IS UNTOUCHED. */
|
|
75
100
|
async forget() {
|
|
101
|
+
this.#assertWritable();
|
|
76
102
|
await this.#ephemeral.forget(this.#ephemeralKey());
|
|
77
103
|
this.#cachedState = null;
|
|
78
104
|
return this;
|
|
@@ -80,7 +106,13 @@ export class Session {
|
|
|
80
106
|
// -- durable half ----------------------------------------------------------
|
|
81
107
|
/** The stored conversation this session is bound to. */
|
|
82
108
|
thread() {
|
|
83
|
-
|
|
109
|
+
this.#assertWritable();
|
|
110
|
+
const store = this.#addressIndex ? indexedStore(this.#rawDurable, this.#addressIndex, this.address, 'durable', 'thread') : this.#durable;
|
|
111
|
+
return new Thread(store, `${this.key()}:thread`);
|
|
112
|
+
}
|
|
113
|
+
async findThread() {
|
|
114
|
+
const thread = this.thread();
|
|
115
|
+
return await thread.exists() ? thread : null;
|
|
84
116
|
}
|
|
85
117
|
/**
|
|
86
118
|
* The agent task list this session is bound to.
|
|
@@ -93,13 +125,15 @@ export class Session {
|
|
|
93
125
|
* against a store as safe as one reached through here.
|
|
94
126
|
*/
|
|
95
127
|
tasks(options = {}) {
|
|
96
|
-
|
|
128
|
+
this.#assertWritable();
|
|
129
|
+
const store = this.#addressIndex ? indexedStore(this.#rawDurable, this.#addressIndex, this.address, 'durable', 'tasks') : this.#durable;
|
|
130
|
+
return new StoreTaskSource(store, `${this.key()}:tasks`, options);
|
|
97
131
|
}
|
|
98
132
|
async capability(name) {
|
|
99
133
|
const stored = (await this.#durable.get(this.#durableKey())) ?? {};
|
|
100
134
|
const capabilities = isJsonObject(stored.capabilities) ? stored.capabilities : {};
|
|
101
135
|
const capability = capabilities[name];
|
|
102
|
-
return isJsonObject(capability) ? capability : null;
|
|
136
|
+
return isJsonObject(capability) ? (this.#resolutionOnly ? structuredClone(capability) : capability) : null;
|
|
103
137
|
}
|
|
104
138
|
async usingCapability(name, state) {
|
|
105
139
|
return this.#writeCapabilities((capabilities) => ({ ...capabilities, [name]: state }));
|
|
@@ -149,7 +183,8 @@ export class Session {
|
|
|
149
183
|
* run inside this, not outside it.
|
|
150
184
|
*/
|
|
151
185
|
async lock(callback, ttlSeconds = 10, waitSeconds = 5) {
|
|
152
|
-
|
|
186
|
+
this.#assertWritable();
|
|
187
|
+
return this.#lockStore.withLock(this.key(), async () => {
|
|
153
188
|
// Re-read inside the lock. State written by whoever held it before us
|
|
154
189
|
// is otherwise invisible to this instance, and acting on a stale read
|
|
155
190
|
// is the thing the lock exists to prevent.
|
|
@@ -164,17 +199,37 @@ export class Session {
|
|
|
164
199
|
#durableKey() {
|
|
165
200
|
return `${this.key()}:durable`;
|
|
166
201
|
}
|
|
202
|
+
/** Refuse explicit logical mutations; ordinary get() retains expired-entry cleanup. */
|
|
203
|
+
async resolveReadOnly(mode, resolve) {
|
|
204
|
+
const view = new Session({ participant: this.participant, scope: this.scope, agent: this.agent, actor: this.actor,
|
|
205
|
+
ephemeral: this.#rawEphemeral, durable: this.#rawDurable, addressIndex: this.#addressIndex, ttlSeconds: this.#ttlSeconds });
|
|
206
|
+
view.#resolutionOnly = true;
|
|
207
|
+
view.#resolvedMode = mode;
|
|
208
|
+
try {
|
|
209
|
+
return await resolve(view);
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
view.#resolutionOnly = false;
|
|
213
|
+
view.#cachedState = null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
#assertWritable() {
|
|
217
|
+
if (this.#resolutionOnly)
|
|
218
|
+
throw new Error('Harness tool resolution is read-only.');
|
|
219
|
+
}
|
|
167
220
|
async #readString(key) {
|
|
168
221
|
const value = (await this.state())[key];
|
|
169
222
|
return typeof value === 'string' ? value : null;
|
|
170
223
|
}
|
|
171
224
|
async #write(key, value) {
|
|
225
|
+
this.#assertWritable();
|
|
172
226
|
const state = { ...(await this.state()), [key]: value };
|
|
173
227
|
await this.#ephemeral.put(this.#ephemeralKey(), state, this.#ttlSeconds);
|
|
174
228
|
this.#cachedState = state;
|
|
175
229
|
return this;
|
|
176
230
|
}
|
|
177
231
|
async #writeCapabilities(mutate) {
|
|
232
|
+
this.#assertWritable();
|
|
178
233
|
const stored = (await this.#durable.get(this.#durableKey())) ?? {};
|
|
179
234
|
const capabilities = isJsonObject(stored.capabilities) ? stored.capabilities : {};
|
|
180
235
|
await this.#durable.put(this.#durableKey(), { ...stored, capabilities: mutate(capabilities) });
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface LockIdentity {
|
|
2
|
+
readonly platform: 'linux' | 'darwin' | 'win32';
|
|
3
|
+
readonly boot_id: string;
|
|
4
|
+
readonly pid_namespace: string | null;
|
|
5
|
+
}
|
|
6
|
+
export interface FileLockIdentityStatus {
|
|
7
|
+
readonly sampled: boolean;
|
|
8
|
+
readonly recovery_available: boolean;
|
|
9
|
+
readonly definitive: boolean;
|
|
10
|
+
readonly retry_after_ms: number | null;
|
|
11
|
+
readonly sample_duration_ms: number;
|
|
12
|
+
readonly code: 'file_lock_identity_unavailable' | null;
|
|
13
|
+
}
|
|
14
|
+
/** Process metadata snapshot only; never runs a query or performs I/O. */
|
|
15
|
+
export declare function fileLockIdentityStatus(): FileLockIdentityStatus;
|
|
16
|
+
/** Definitive metadata is permanent; transient failure is sampled at most once per minute. */
|
|
17
|
+
export declare function nativeLockIdentity(): LockIdentity | null;
|
|
18
|
+
export declare function sameLockIdentity(owner: LockIdentity, local: LockIdentity): boolean;
|
|
19
|
+
/** Read-only signal-zero probe. Permission errors and reused live PIDs remain held. */
|
|
20
|
+
export declare function lockPidIsDead(pid: number): boolean;
|