@particle-academy/prism-harness 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -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/errors.d.ts +5 -0
- package/dist/errors.js +11 -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 +95 -8
- 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/harness.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { Session } from './session.js';
|
|
2
2
|
import { MemorySessionStore } from './stores/memory-session-store.js';
|
|
3
3
|
import { SessionStoreManager } from './stores/store-manager.js';
|
|
4
|
+
import { ParticipantAdapters, ParticipantRef, SessionAddress, validateAgent } from './address.js';
|
|
5
|
+
import { addressLimit, indexedStore, StoreAddressIndex } from './address-index.js';
|
|
6
|
+
import { StoreTaskSource } from './store-task-source.js';
|
|
7
|
+
import { Thread } from './thread.js';
|
|
4
8
|
/**
|
|
5
9
|
* The entry point: `harness.for(participant).session('support')`.
|
|
6
10
|
*
|
|
@@ -24,8 +28,25 @@ import { SessionStoreManager } from './stores/store-manager.js';
|
|
|
24
28
|
export class PrismHarness {
|
|
25
29
|
#stores;
|
|
26
30
|
#ttlSeconds;
|
|
31
|
+
#participants;
|
|
32
|
+
#maxAddresses;
|
|
33
|
+
#runtime;
|
|
34
|
+
#agents;
|
|
35
|
+
#defaultScope;
|
|
36
|
+
#defaultTaskList;
|
|
27
37
|
constructor(options = {}) {
|
|
38
|
+
this.#runtime = options.runtime;
|
|
39
|
+
this.#defaultScope = options.defaultScope ?? 'default';
|
|
40
|
+
this.#defaultTaskList = options.defaultTaskList ?? 'default';
|
|
41
|
+
validateHarnessConfig({ defaultScope: this.#defaultScope, defaultTaskList: this.#defaultTaskList });
|
|
42
|
+
this.#agents = Object.fromEntries(Object.entries(options.agents ?? {}).map(([agent, config]) => {
|
|
43
|
+
validateAgent(agent);
|
|
44
|
+
validateHarnessConfig(config);
|
|
45
|
+
return [agent, { ...config }];
|
|
46
|
+
}));
|
|
28
47
|
this.#ttlSeconds = options.ttlSeconds ?? null;
|
|
48
|
+
this.#participants = options.participantAdapters ?? new ParticipantAdapters();
|
|
49
|
+
this.#maxAddresses = addressLimit(options.maxAddressesPerParticipant ?? null);
|
|
29
50
|
this.#stores = new SessionStoreManager({
|
|
30
51
|
stores: options.stores,
|
|
31
52
|
drivers: options.drivers ?? { default: () => new MemorySessionStore() },
|
|
@@ -33,7 +54,36 @@ export class PrismHarness {
|
|
|
33
54
|
}
|
|
34
55
|
/** Bind to a participant. Returns a builder, because the scope comes next. */
|
|
35
56
|
for(participant) {
|
|
36
|
-
return
|
|
57
|
+
return this.pending(participant, null);
|
|
58
|
+
}
|
|
59
|
+
forAgent(agent) {
|
|
60
|
+
validateAgent(agent);
|
|
61
|
+
return new AgentHarness(this, agent);
|
|
62
|
+
}
|
|
63
|
+
effectiveTools(agent, mode, participant, scope) {
|
|
64
|
+
if (!this.#runtime)
|
|
65
|
+
throw new Error('Tool inspection requires a configured Harness runtime.');
|
|
66
|
+
return this.#runtime.effectiveTools(this.pending(participant, agent).session(scope), mode);
|
|
67
|
+
}
|
|
68
|
+
pending(participant, agent, actor) {
|
|
69
|
+
validateAgent(agent);
|
|
70
|
+
const config = agent === null ? undefined : this.#agents[agent];
|
|
71
|
+
const ttl = config?.ttlSeconds === undefined ? this.#ttlSeconds : config.ttlSeconds;
|
|
72
|
+
return new PendingSession(participant, this.#stores, ttl, agent, actor, this.#participants, this.#maxAddresses, config?.defaultScope ?? this.#defaultScope);
|
|
73
|
+
}
|
|
74
|
+
tasks(list, agent = null) {
|
|
75
|
+
list ??= (agent === null ? undefined : this.#agents[agent]?.defaultTaskList) ?? this.#defaultTaskList;
|
|
76
|
+
const address = new SessionAddress(null, `task-list:${list}`, agent);
|
|
77
|
+
const durable = this.#stores.durable();
|
|
78
|
+
return new StoreTaskSource(agent === null ? durable : indexedStore(durable, new StoreAddressIndex(durable, this.#maxAddresses), address, 'durable', 'tasks'), agent === null ? `${list}:tasks` : `agent-list:${address.indexKey()}:tasks`);
|
|
79
|
+
}
|
|
80
|
+
sharedThread(scope, agent = null) {
|
|
81
|
+
const address = new SessionAddress(null, scope, agent), durable = this.#stores.durable();
|
|
82
|
+
return new Thread(agent === null ? durable : indexedStore(durable, new StoreAddressIndex(durable, this.#maxAddresses), address, 'durable', 'thread'), `thread:${address.threadKey()}`);
|
|
83
|
+
}
|
|
84
|
+
async findShared(scope, agent = null) {
|
|
85
|
+
const thread = this.sharedThread(scope, agent);
|
|
86
|
+
return await thread.exists() ? thread : null;
|
|
37
87
|
}
|
|
38
88
|
ephemeralStore() {
|
|
39
89
|
return this.#stores.ephemeral();
|
|
@@ -46,10 +96,25 @@ export class PendingSession {
|
|
|
46
96
|
participant;
|
|
47
97
|
stores;
|
|
48
98
|
ttlSeconds;
|
|
49
|
-
|
|
99
|
+
agent;
|
|
100
|
+
actor;
|
|
101
|
+
participants;
|
|
102
|
+
maxAddresses;
|
|
103
|
+
defaultScope;
|
|
104
|
+
constructor(participant, stores, ttlSeconds, agent = null, actor, participants = new ParticipantAdapters(), maxAddresses = null, defaultScope = 'default') {
|
|
50
105
|
this.participant = participant;
|
|
51
106
|
this.stores = stores;
|
|
52
107
|
this.ttlSeconds = ttlSeconds;
|
|
108
|
+
this.agent = agent;
|
|
109
|
+
this.actor = actor;
|
|
110
|
+
this.participants = participants;
|
|
111
|
+
this.maxAddresses = maxAddresses;
|
|
112
|
+
this.defaultScope = defaultScope;
|
|
113
|
+
}
|
|
114
|
+
asActor(actor) {
|
|
115
|
+
if (this.participant instanceof ParticipantRef)
|
|
116
|
+
throw new Error('Typed participants require a registered actor adapter.');
|
|
117
|
+
return new PendingSession(this.participant, this.stores, this.ttlSeconds, this.agent, actor, this.participants, this.maxAddresses, this.defaultScope);
|
|
53
118
|
}
|
|
54
119
|
/**
|
|
55
120
|
* Resolve the session for a scope.
|
|
@@ -58,13 +123,43 @@ export class PendingSession {
|
|
|
58
123
|
* guard fire when a session is opened rather than at some later moment when
|
|
59
124
|
* an approval needs saving.
|
|
60
125
|
*/
|
|
61
|
-
session(scope) {
|
|
126
|
+
session(scope = this.defaultScope) {
|
|
127
|
+
const actor = this.participant instanceof ParticipantRef ? this.participants.actor(this.participant, this.actor) : (this.actor ?? this.participant);
|
|
128
|
+
const durable = this.stores.durable();
|
|
62
129
|
return new Session({
|
|
63
130
|
participant: this.participant,
|
|
64
131
|
scope,
|
|
65
132
|
ephemeral: this.stores.ephemeral(),
|
|
66
|
-
durable
|
|
133
|
+
durable,
|
|
67
134
|
ttlSeconds: this.ttlSeconds,
|
|
135
|
+
agent: this.agent,
|
|
136
|
+
actor,
|
|
137
|
+
addressIndex: this.agent !== null || this.participant instanceof ParticipantRef ? new StoreAddressIndex(durable, this.maxAddresses) : undefined,
|
|
68
138
|
});
|
|
69
139
|
}
|
|
70
140
|
}
|
|
141
|
+
/** A namespace-bound view, sharing stores without a mutable current-agent flag. */
|
|
142
|
+
export class AgentHarness {
|
|
143
|
+
harness;
|
|
144
|
+
agent;
|
|
145
|
+
constructor(harness, agent) {
|
|
146
|
+
this.harness = harness;
|
|
147
|
+
this.agent = agent;
|
|
148
|
+
validateAgent(agent);
|
|
149
|
+
Object.defineProperty(this, 'agent', { writable: false, configurable: false });
|
|
150
|
+
}
|
|
151
|
+
for(participant) { return this.harness.pending(participant, this.agent); }
|
|
152
|
+
effectiveTools(mode, participant, scope) { return this.harness.effectiveTools(this.agent, mode, participant, scope); }
|
|
153
|
+
tasks(list) { return this.harness.tasks(list, this.agent); }
|
|
154
|
+
sharedThread(scope) { return this.harness.sharedThread(scope, this.agent); }
|
|
155
|
+
findShared(scope) { return this.harness.findShared(scope, this.agent); }
|
|
156
|
+
}
|
|
157
|
+
function validateHarnessConfig(config) {
|
|
158
|
+
if (config.ttlSeconds !== undefined && config.ttlSeconds !== null && (!Number.isSafeInteger(config.ttlSeconds) || config.ttlSeconds < 0))
|
|
159
|
+
throw new TypeError('Agent ephemeral TTL must be nonnegative whole seconds or null.');
|
|
160
|
+
for (const key of ['defaultScope', 'defaultTaskList']) {
|
|
161
|
+
const value = config[key];
|
|
162
|
+
if (value !== undefined && (typeof value !== 'string' || Buffer.from(value, 'utf8').toString('utf8') !== value || (key === 'defaultTaskList' && value.length === 0)))
|
|
163
|
+
throw new TypeError('Agent scope/list defaults must be valid strings.');
|
|
164
|
+
}
|
|
165
|
+
}
|
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) {
|
|
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);
|
|
@@ -171,7 +222,8 @@ export class AgentRuntime {
|
|
|
171
222
|
text = response.text;
|
|
172
223
|
finishReason = response.finishReason;
|
|
173
224
|
const toolCalls = response.toolCalls ?? [];
|
|
174
|
-
const
|
|
225
|
+
const failedFinish = finishReason === 'error' || finishReason === 'unknown' ? finishReason : null;
|
|
226
|
+
const gated = failedFinish === null ? toolCalls.filter((call) => mode.needsApproval(call.name)) : [];
|
|
175
227
|
const requests = gated.map((call) => ({
|
|
176
228
|
approval_id: `apr_${randomUUID().replaceAll('-', '')}`,
|
|
177
229
|
tool_call_id: call.id,
|
|
@@ -184,6 +236,14 @@ export class AgentRuntime {
|
|
|
184
236
|
? { ...(response.additionalContent ?? {}), structured: response.structured ?? null }
|
|
185
237
|
: response.additionalContent;
|
|
186
238
|
await thread.record([assistantRow(response.text, toolCalls, metadata, requests)], runId);
|
|
239
|
+
if (failedFinish !== null) {
|
|
240
|
+
// Complete the transcript with refusals, never approval requests, so a
|
|
241
|
+
// later resume cannot dispatch calls from this failed response.
|
|
242
|
+
if (toolCalls.length > 0) {
|
|
243
|
+
await thread.record([toolResultRow(toolCalls.map(call => toolResultEntry(call, `Not run: model step failed (${failedFinish}).`)))], runId);
|
|
244
|
+
}
|
|
245
|
+
throw HarnessError.modelStepFailed(failedFinish);
|
|
246
|
+
}
|
|
187
247
|
if (toolCalls.length === 0) {
|
|
188
248
|
if (schema !== undefined) {
|
|
189
249
|
// Recorded first, then checked: the exchange happened either way, and
|
|
@@ -270,7 +330,7 @@ export class AgentRuntime {
|
|
|
270
330
|
}
|
|
271
331
|
}
|
|
272
332
|
await live.thread().record([toolResultRow([...work.results.values(), ...resolved], [...work.decisions.values()])], runId);
|
|
273
|
-
}, RESOLUTION_LOCK_SECONDS, RESOLUTION_WAIT_SECONDS);
|
|
333
|
+
}, this.#setting(session, 'lock_ttl') ?? RESOLUTION_LOCK_SECONDS, this.#setting(session, 'lock_wait') ?? RESOLUTION_WAIT_SECONDS);
|
|
274
334
|
}
|
|
275
335
|
/**
|
|
276
336
|
* The gated calls of the last tool-calling turn that have no result yet, with
|
|
@@ -389,6 +449,33 @@ export class AgentRuntime {
|
|
|
389
449
|
*
|
|
390
450
|
* `approvalId` is `PendingApproval.id`, not the tool call id.
|
|
391
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
|
+
}
|
|
392
479
|
export async function recordApproval(session, approvalId, approved, reason = null) {
|
|
393
480
|
const run = await session.run();
|
|
394
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
|
}
|