@orkestrel/agent 0.0.19 → 0.0.20
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 +4 -4
- package/dist/src/core/index.cjs +1176 -836
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +1210 -783
- package/dist/src/core/index.d.ts +1210 -783
- package/dist/src/core/index.js +1162 -837
- package/dist/src/core/index.js.map +1 -1
- package/package.json +19 -20
package/dist/src/core/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { attempt, isArray, isBoolean, isFiniteNumber, isObject, isRecord, isString, parseJSONValue, rawShape, stringShape } from "@orkestrel/contract";
|
|
2
2
|
import { createDatabase, createMemoryDriver } from "@orkestrel/database";
|
|
3
3
|
import { createQueue } from "@orkestrel/queue";
|
|
4
|
-
import {
|
|
4
|
+
import { createRunner, errorToMessage } from "@orkestrel/workflow";
|
|
5
5
|
import { createAbort } from "@orkestrel/abort";
|
|
6
6
|
import { createTimeout } from "@orkestrel/timeout";
|
|
7
7
|
import { Emitter } from "@orkestrel/emitter";
|
|
@@ -10,14 +10,14 @@ import { WorkspaceManager, isBinary, isText } from "@orkestrel/workspace";
|
|
|
10
10
|
import { createTokenBudget } from "@orkestrel/budget";
|
|
11
11
|
//#region src/core/constants.ts
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* Caps an {@link AgentInterface} turn's tool iterations by default — the maximum
|
|
14
14
|
* number of context → provider → tools cycles before the loop stops, so a model that
|
|
15
|
-
* keeps requesting tools can never loop forever. Overridable per agent
|
|
15
|
+
* keeps requesting tools can never loop forever. Overridable per agent through
|
|
16
16
|
* `AgentOptions.limit`.
|
|
17
17
|
*/
|
|
18
18
|
var DEFAULT_AGENT_LIMIT = 10;
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
20
|
+
* Names the zone an {@link AuthorityInterface}'s default fallback {@link AuthorityDecision}
|
|
21
21
|
* carries — the classification for a tool call that matched no rule. Paired with the
|
|
22
22
|
* default `allowed: true` fallback, an unmatched call is allowed under this zone, so a
|
|
23
23
|
* rules list of denials acts as a denylist; a caller wanting deny-by-default supplies
|
|
@@ -25,16 +25,16 @@ var DEFAULT_AGENT_LIMIT = 10;
|
|
|
25
25
|
*/
|
|
26
26
|
var DEFAULT_AUTHORITY_ZONE = "default";
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
28
|
+
* Sets the default number of recent live messages a {@link ConversationInterface}'s `compact()`
|
|
29
29
|
* RETAINS verbatim — `0`, so a manual `compact()` folds ALL of the current live messages
|
|
30
30
|
* into one summarized section (no tail kept). A caller retains a recent tail by passing
|
|
31
31
|
* `keep` (on {@link ConversationOptions}, {@link ConversationManagerOptions}, or per-fold
|
|
32
|
-
*
|
|
32
|
+
* through {@link CompactOptions}), folding only the older `count - keep` messages and leaving
|
|
33
33
|
* the most recent `keep` live for the next turn. Overridable everywhere `keep` is accepted.
|
|
34
34
|
*/
|
|
35
35
|
var DEFAULT_CONVERSATION_KEEP = 0;
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
37
|
+
* Names the framing label a {@link ConversationInterface}'s `view()` prefixes onto each compacted
|
|
38
38
|
* section's summary so a small model reads it as a CONDENSED RECAP of earlier turns — not a
|
|
39
39
|
* literal assistant turn to echo or treat as the live answer.
|
|
40
40
|
*
|
|
@@ -49,40 +49,40 @@ var DEFAULT_CONVERSATION_KEEP = 0;
|
|
|
49
49
|
*/
|
|
50
50
|
var CONVERSATION_RECAP_PREFIX = "[Summary of earlier messages] ";
|
|
51
51
|
/**
|
|
52
|
-
*
|
|
52
|
+
* Names the opening tag a {@link import('./ThinkSplitter.js').ThinkSplitter} recognizes as the start of
|
|
53
53
|
* an in-content reasoning span — the de-facto wire convention thinking models (qwen3, DeepSeek-R1
|
|
54
54
|
* family) emit their chain-of-thought under when a daemon renders it inline instead of on a
|
|
55
55
|
* separate wire field. Paired with {@link THINK_CLOSE}.
|
|
56
56
|
*/
|
|
57
57
|
var THINK_OPEN = "<think>";
|
|
58
58
|
/**
|
|
59
|
-
*
|
|
59
|
+
* Names the closing tag that ends a {@link THINK_OPEN} reasoning span. A span the stream never closes
|
|
60
60
|
* (the model was cut off mid-reasoning) is treated as thinking to its end —
|
|
61
61
|
* {@link import('./types.js').ThinkSplitterInterface.flush} settles it.
|
|
62
62
|
*/
|
|
63
63
|
var THINK_CLOSE = "</think>";
|
|
64
64
|
/**
|
|
65
|
-
*
|
|
65
|
+
* Names the section header {@link import('./AgentContext.js').AgentContext}'s `build()` renders the
|
|
66
66
|
* ACTIVE workspace's TEXT files under — the leading line of the dedicated workspace block in the
|
|
67
67
|
* system message, the carrier-split counterpart to the documents / images section headers.
|
|
68
68
|
*
|
|
69
69
|
* @remarks
|
|
70
70
|
* `build()` OWNS the workspace render (a `Workspace` / `WorkspaceManager` stays file-focused — no
|
|
71
|
-
* `
|
|
71
|
+
* `open` / `format` getters), so this header lives here as the agents module's one
|
|
72
72
|
* workspace-section framing constant rather than on a manager. Each workspace text file renders
|
|
73
73
|
* beneath it as a fenced `` File: <path>\n```<language>\n<text>\n``` `` block — the SAME framing
|
|
74
|
-
* the documents section uses — placed
|
|
74
|
+
* the documents section uses — placed immediately after the documents section in the system block.
|
|
75
75
|
*/
|
|
76
76
|
var WORKSPACE_SECTION_HEADER = "## Workspace";
|
|
77
77
|
/**
|
|
78
|
-
*
|
|
78
|
+
* Estimates the per-message role/framing overhead {@link import('./helpers.js').estimateMessages}
|
|
79
79
|
* adds on top of a message's content estimate — accounts for the fixed wire framing every
|
|
80
80
|
* conversation turn carries (its role tag, delimiters) that {@link import('./helpers.js').estimateTokens}'s
|
|
81
81
|
* content-only heuristic does not otherwise capture.
|
|
82
82
|
*/
|
|
83
83
|
var MESSAGE_TOKEN_OVERHEAD = 4;
|
|
84
84
|
/**
|
|
85
|
-
*
|
|
85
|
+
* Names the coarse, deliberately-approximate per-image token cost {@link import('./helpers.js').estimateMessages}
|
|
86
86
|
* charges for each attached image.
|
|
87
87
|
*
|
|
88
88
|
* @remarks
|
|
@@ -95,16 +95,19 @@ var IMAGE_TOKEN_ESTIMATE = 512;
|
|
|
95
95
|
//#endregion
|
|
96
96
|
//#region src/core/errors.ts
|
|
97
97
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* before the cancel.
|
|
98
|
+
* Reports a provider stream cancelled mid-flight by its bound signal — thrown by a
|
|
99
|
+
* {@link ProviderInterface}'s `stream`, carrying the {@link ProviderResult} assembled
|
|
100
|
+
* from whatever streamed before the cancel.
|
|
101
101
|
*
|
|
102
102
|
* @remarks
|
|
103
103
|
* Lets a caller recover the partial content (and any tool calls / usage seen so far)
|
|
104
104
|
* on cancellation: `catch` the throw, narrow with {@link isProviderAbortError}, and
|
|
105
|
-
* read `partial`.
|
|
105
|
+
* read `partial`. `code` is the machine-readable condition (`'ABORT'` — the only one this
|
|
106
|
+
* error reports), so a `catch` branches on it rather than on the message string.
|
|
106
107
|
*/
|
|
107
108
|
var ProviderAbortError = class extends Error {
|
|
109
|
+
/** Names the machine-readable condition — `'ABORT'`: a stream cancelled mid-flight. */
|
|
110
|
+
code = "ABORT";
|
|
108
111
|
partial;
|
|
109
112
|
constructor(partial) {
|
|
110
113
|
super("provider stream aborted");
|
|
@@ -113,10 +116,10 @@ var ProviderAbortError = class extends Error {
|
|
|
113
116
|
}
|
|
114
117
|
};
|
|
115
118
|
/**
|
|
116
|
-
*
|
|
119
|
+
* Narrows an unknown caught value to a {@link ProviderAbortError}.
|
|
117
120
|
*
|
|
118
121
|
* @param value - The value to test (typically a `catch` binding)
|
|
119
|
-
* @returns
|
|
122
|
+
* @returns True if `value` is a {@link ProviderAbortError}; false otherwise
|
|
120
123
|
*
|
|
121
124
|
* @example
|
|
122
125
|
* ```ts
|
|
@@ -131,9 +134,9 @@ function isProviderAbortError(value) {
|
|
|
131
134
|
return value instanceof ProviderAbortError;
|
|
132
135
|
}
|
|
133
136
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* `
|
|
137
|
+
* Reports an {@link AgentInterface} run that ended {@link AgentResult.partial} under a
|
|
138
|
+
* `partial` policy of `false` (the default) — thrown by an agent-job handler (a
|
|
139
|
+
* `createAgentQueue` / `createAgentRunner` job), carrying the partial
|
|
137
140
|
* {@link AgentResult} so the failure stays inspectable.
|
|
138
141
|
*
|
|
139
142
|
* @remarks
|
|
@@ -141,12 +144,16 @@ function isProviderAbortError(value) {
|
|
|
141
144
|
* runner abort threaded in, a `timeout` deadline, or an exhausted token `budget`) rather
|
|
142
145
|
* than finishing naturally. For a durable JOB that is a failure by default: throwing this
|
|
143
146
|
* lets the Queue's retries re-run the job and a Runner's fail-fast abort its siblings.
|
|
144
|
-
* Set `
|
|
147
|
+
* Set `partial: true` (see `AgentQueueOptions` / `AgentRunnerOptions`) to treat a
|
|
145
148
|
* partial as success instead, in which case this is never thrown. Narrow a caught value
|
|
146
|
-
* with {@link isAgentJobError} to read `partial`.
|
|
149
|
+
* with {@link isAgentJobError} to read `partial`. `code` is the machine-readable condition
|
|
150
|
+
* (`'PARTIAL'` — the only one this error reports), so a `catch` branches on it rather than on
|
|
151
|
+
* the message string.
|
|
147
152
|
*/
|
|
148
153
|
var AgentJobError = class extends Error {
|
|
149
|
-
/**
|
|
154
|
+
/** Names the machine-readable condition — `'PARTIAL'`: a job that ended partial under a disallowing policy. */
|
|
155
|
+
code = "PARTIAL";
|
|
156
|
+
/** Holds the partial {@link AgentResult} the cancelled job produced. */
|
|
150
157
|
partial;
|
|
151
158
|
constructor(message, partial) {
|
|
152
159
|
super(message);
|
|
@@ -155,10 +162,10 @@ var AgentJobError = class extends Error {
|
|
|
155
162
|
}
|
|
156
163
|
};
|
|
157
164
|
/**
|
|
158
|
-
*
|
|
165
|
+
* Narrows an unknown caught value to an {@link AgentJobError}.
|
|
159
166
|
*
|
|
160
167
|
* @param value - The value to test (typically a `catch` binding or a rejected enqueue)
|
|
161
|
-
* @returns
|
|
168
|
+
* @returns True if `value` is an {@link AgentJobError}; false otherwise
|
|
162
169
|
*
|
|
163
170
|
* @example
|
|
164
171
|
* ```ts
|
|
@@ -173,22 +180,22 @@ function isAgentJobError(value) {
|
|
|
173
180
|
return value instanceof AgentJobError;
|
|
174
181
|
}
|
|
175
182
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
183
|
+
* Reports a conversation with no {@link ConversationSummaryHandler} to fold its messages
|
|
184
|
+
* with, or with a structurally invalid `sections` cap — thrown by a
|
|
185
|
+
* {@link ConversationInterface}'s `compact()`, carrying a machine-readable `code`.
|
|
179
186
|
*
|
|
180
187
|
* @remarks
|
|
181
188
|
* Compaction REQUIRES a summarizer (it digests the folded slice into a section summary and
|
|
182
189
|
* regenerates the rollup); a conversation created without one can still store + `view()` its
|
|
183
|
-
* live tail, but a `compact()` is a programmer error
|
|
184
|
-
* A `sections` cap (
|
|
190
|
+
* live tail, but a `compact()` is a programmer error and throws this with `'SUMMARIZER'`.
|
|
191
|
+
* A `sections` cap (on {@link import('./types.js').ConversationOptions} /
|
|
185
192
|
* {@link import('./types.js').ConversationManagerOptions} /
|
|
186
193
|
* {@link import('./types.js').CompactOptions}) must be `>= 1` — a sub-1 cap is a programmer
|
|
187
194
|
* error and throws this with `'SECTIONS'`. Narrow a caught value with
|
|
188
195
|
* {@link isConversationError} and branch on `error.code`.
|
|
189
196
|
*/
|
|
190
197
|
var ConversationError = class extends Error {
|
|
191
|
-
/**
|
|
198
|
+
/** Names the machine-readable condition — `'SUMMARIZER'`: a `compact()` with no summarizer; `'SECTIONS'`: a sub-1 `sections` cap. */
|
|
192
199
|
code;
|
|
193
200
|
constructor(code, message) {
|
|
194
201
|
super(message);
|
|
@@ -197,10 +204,10 @@ var ConversationError = class extends Error {
|
|
|
197
204
|
}
|
|
198
205
|
};
|
|
199
206
|
/**
|
|
200
|
-
*
|
|
207
|
+
* Narrows an unknown caught value to a {@link ConversationError}.
|
|
201
208
|
*
|
|
202
209
|
* @param value - The value to test (typically a `catch` binding)
|
|
203
|
-
* @returns
|
|
210
|
+
* @returns True if `value` is a {@link ConversationError}; false otherwise
|
|
204
211
|
*
|
|
205
212
|
* @example
|
|
206
213
|
* ```ts
|
|
@@ -215,19 +222,22 @@ function isConversationError(value) {
|
|
|
215
222
|
return value instanceof ConversationError;
|
|
216
223
|
}
|
|
217
224
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
225
|
+
* Reports a concurrent run that would corrupt SHARED per-agent accounting, or a rehydration
|
|
226
|
+
* name absent from its registry pool — thrown synchronously by an {@link AgentInterface}'s
|
|
227
|
+
* `stream()` and by an {@link AgentRegistryInterface}'s accessors, carrying a machine-readable
|
|
228
|
+
* `code`.
|
|
220
229
|
*
|
|
221
230
|
* @remarks
|
|
222
|
-
*
|
|
223
|
-
* context budget) or a construction-level `budget` with no per-run override
|
|
224
|
-
* budget)
|
|
225
|
-
* shared instance
|
|
226
|
-
*
|
|
227
|
-
*
|
|
231
|
+
* `'CONCURRENCY'` reports a run already in flight on the same agent, PLUS a construction-level
|
|
232
|
+
* `window` (a shared context budget) or a construction-level `budget` with no per-run override
|
|
233
|
+
* (a shared cost budget) — a second concurrent `stream()` would race its charges against the
|
|
234
|
+
* same shared instance, corrupting the accounting. Use separate agents, or per-run `budget`
|
|
235
|
+
* overrides with no `window`, for genuinely concurrent runs. `'REGISTRY'` reports a rehydration
|
|
236
|
+
* name absent from its registry pool, on `provider` / `tool` / `authority` / `scheduler` /
|
|
237
|
+
* `build`. Narrow a caught value with {@link isAgentError} and branch on `error.code`.
|
|
228
238
|
*/
|
|
229
239
|
var AgentError = class extends Error {
|
|
230
|
-
/**
|
|
240
|
+
/** Names the machine-readable condition — `'CONCURRENCY'`: a concurrent run on a shared accounting agent; `'REGISTRY'`: a rehydration name absent from its registry pool. */
|
|
231
241
|
code;
|
|
232
242
|
constructor(code, message) {
|
|
233
243
|
super(message);
|
|
@@ -236,10 +246,10 @@ var AgentError = class extends Error {
|
|
|
236
246
|
}
|
|
237
247
|
};
|
|
238
248
|
/**
|
|
239
|
-
*
|
|
249
|
+
* Narrows an unknown caught value to an {@link AgentError}.
|
|
240
250
|
*
|
|
241
251
|
* @param value - The value to test (typically a `catch` binding)
|
|
242
|
-
* @returns
|
|
252
|
+
* @returns True if `value` is an {@link AgentError}; false otherwise
|
|
243
253
|
*
|
|
244
254
|
* @example
|
|
245
255
|
* ```ts
|
|
@@ -254,335 +264,9 @@ function isAgentError(value) {
|
|
|
254
264
|
return value instanceof AgentError;
|
|
255
265
|
}
|
|
256
266
|
//#endregion
|
|
257
|
-
//#region src/core/conversations/Conversation.ts
|
|
258
|
-
/**
|
|
259
|
-
* A conversation grouping messages ABOVE a flat message store — a live uncompacted tail it
|
|
260
|
-
* OWNS DIRECTLY plus compacted, summarized {@link SectionInterface}s and a regenerated rollup
|
|
261
|
-
* `summary`, with on-demand `rehydrate` and substring `search`, driven by a provider-agnostic
|
|
262
|
-
* {@link ConversationSummarizer} seam (so `core` never imports a provider).
|
|
263
|
-
*
|
|
264
|
-
* @remarks
|
|
265
|
-
* - **Live tail + sections.** The conversation OWNS its live tail DIRECTLY — `#messages` is an
|
|
266
|
-
* insertion-ordered `Map` of immutable {@link MessageInterface}s keyed by their minted id
|
|
267
|
-
* (the SAME store mechanics a flat manager had, folded in: `add` / `message` / `messages` /
|
|
268
|
-
* `remove` / `clear` / `count`), exactly as a `Workspace` owns its files (no separate
|
|
269
|
-
* per-value manager). `#sections` are the compacted history (oldest → newest), each a
|
|
270
|
-
* summarized slice that RETAINS its originals. `#summary` is the rollup (a
|
|
271
|
-
* summary-of-summaries over all sections), regenerated on each compaction (`undefined`
|
|
272
|
-
* until the first).
|
|
273
|
-
* - **`view()`.** Each section folds to ONE synthetic summary message (role `'assistant'` — a
|
|
274
|
-
* prior-context recap — keyed by the section's stable `id`), then the live messages
|
|
275
|
-
* verbatim. The rollup `summary` is NOT injected (it is separately pull-able); `view()`
|
|
276
|
-
* carries the per-section summaries, which ARE the compaction benefit.
|
|
277
|
-
* - **`compact()`.** Folds the oldest `count - keep` live messages into a new section
|
|
278
|
-
* (its `summary` from `#summarize`), removes them from the live tail by id, regenerates the
|
|
279
|
-
* rollup (a SECOND `#summarize` over all section summaries), and emits `summary` then
|
|
280
|
-
* `compact`. Returns the section, or `undefined` when nothing folds (`count <= keep`).
|
|
281
|
-
* THROWS a {@link ConversationError} when no `#summarize` was supplied. Two summarizer calls
|
|
282
|
-
* per compaction.
|
|
283
|
-
* - **`rehydrate(id)` / `search(query)`.** `rehydrate` returns a section's full original
|
|
284
|
-
* messages (`[]` for an unknown id) and emits `rehydrate` — a pure read (v1 never
|
|
285
|
-
* auto-reinserts). `search` is a case-insensitive substring scan of `content` across ALL
|
|
286
|
-
* messages (every section's originals + the live tail).
|
|
287
|
-
* - **Observable (§13).** The owned {@link emitter} ({@link ConversationEventMap}) carries
|
|
288
|
-
* `compact` / `summary` / `rehydrate`, emitted directly, strictly AFTER the state change;
|
|
289
|
-
* the emitter isolates a listener throw and routes it to its `error` handler (the `error`
|
|
290
|
-
* option), so a buggy observer can never corrupt a compaction.
|
|
291
|
-
*
|
|
292
|
-
* @example
|
|
293
|
-
* ```ts
|
|
294
|
-
* const conversation = new Conversation({ summarize: async (m) => `recap of ${m.length}` })
|
|
295
|
-
* conversation.add([
|
|
296
|
-
* { role: 'user', content: 'Hello' },
|
|
297
|
-
* { role: 'assistant', content: 'Hi there' },
|
|
298
|
-
* ])
|
|
299
|
-
* const section = await conversation.compact() // folds both into one summarized section
|
|
300
|
-
* conversation.view() // [{ role: 'assistant', content: 'recap of 2' }] — the live tail is empty
|
|
301
|
-
* conversation.summary // 'recap of 1' — the rollup over the one section
|
|
302
|
-
* ```
|
|
303
|
-
*/
|
|
304
|
-
var Conversation = class {
|
|
305
|
-
#id;
|
|
306
|
-
#emitter;
|
|
307
|
-
#summarize;
|
|
308
|
-
#keep;
|
|
309
|
-
#cap;
|
|
310
|
-
#sections = [];
|
|
311
|
-
#summary;
|
|
312
|
-
#messages = /* @__PURE__ */ new Map();
|
|
313
|
-
constructor(options, seed) {
|
|
314
|
-
this.#id = seed?.id ?? options?.id ?? crypto.randomUUID();
|
|
315
|
-
this.#emitter = new Emitter({
|
|
316
|
-
...options?.on === void 0 ? {} : { on: options.on },
|
|
317
|
-
...options?.error === void 0 ? {} : { error: options.error }
|
|
318
|
-
});
|
|
319
|
-
this.#summarize = options?.summarize;
|
|
320
|
-
this.#keep = options?.keep ?? 0;
|
|
321
|
-
if (options?.sections !== void 0 && options.sections < 1) throw new ConversationError("SECTIONS", "a sections cap must be >= 1");
|
|
322
|
-
this.#cap = options?.sections;
|
|
323
|
-
if (seed) {
|
|
324
|
-
this.#summary = seed.summary;
|
|
325
|
-
for (const section of seed.sections) this.#sections.push(section);
|
|
326
|
-
for (const message of seed.messages) this.#messages.set(message.id, message);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
get id() {
|
|
330
|
-
return this.#id;
|
|
331
|
-
}
|
|
332
|
-
get emitter() {
|
|
333
|
-
return this.#emitter;
|
|
334
|
-
}
|
|
335
|
-
get summary() {
|
|
336
|
-
return this.#summary;
|
|
337
|
-
}
|
|
338
|
-
get sections() {
|
|
339
|
-
return [...this.#sections];
|
|
340
|
-
}
|
|
341
|
-
get summarizable() {
|
|
342
|
-
return this.#summarize !== void 0;
|
|
343
|
-
}
|
|
344
|
-
get count() {
|
|
345
|
-
return this.#messages.size;
|
|
346
|
-
}
|
|
347
|
-
add(input) {
|
|
348
|
-
if (isArray(input)) return input.map((one) => this.#create(one));
|
|
349
|
-
return this.#create(input);
|
|
350
|
-
}
|
|
351
|
-
message(id) {
|
|
352
|
-
return this.#messages.get(id);
|
|
353
|
-
}
|
|
354
|
-
messages() {
|
|
355
|
-
return [...this.#messages.values()];
|
|
356
|
-
}
|
|
357
|
-
remove(ids) {
|
|
358
|
-
if (isArray(ids)) {
|
|
359
|
-
let removed = false;
|
|
360
|
-
for (const id of ids) if (this.#messages.delete(id)) removed = true;
|
|
361
|
-
return removed;
|
|
362
|
-
}
|
|
363
|
-
return this.#messages.delete(ids);
|
|
364
|
-
}
|
|
365
|
-
clear() {
|
|
366
|
-
this.#messages.clear();
|
|
367
|
-
}
|
|
368
|
-
view() {
|
|
369
|
-
return [...this.#sections.map((section) => this.#recapMessage(section)), ...this.#messages.values()];
|
|
370
|
-
}
|
|
371
|
-
async compact(options) {
|
|
372
|
-
const summarize = this.#summarize;
|
|
373
|
-
if (summarize === void 0) throw new ConversationError("SUMMARIZER", "cannot compact a conversation without a summarizer");
|
|
374
|
-
const cap = options?.sections ?? this.#cap;
|
|
375
|
-
if (cap !== void 0 && cap < 1) throw new ConversationError("SECTIONS", "a sections cap must be >= 1");
|
|
376
|
-
const keep = options?.keep ?? this.#keep;
|
|
377
|
-
const live = [...this.#messages.values()];
|
|
378
|
-
const fold = keep <= 0 ? live.length : live.length - keep;
|
|
379
|
-
if (fold <= 0) return void 0;
|
|
380
|
-
const slice = live.slice(0, fold);
|
|
381
|
-
const summary = await summarize(slice);
|
|
382
|
-
const section = {
|
|
383
|
-
id: crypto.randomUUID(),
|
|
384
|
-
summary,
|
|
385
|
-
messages: slice
|
|
386
|
-
};
|
|
387
|
-
for (const message of slice) this.#messages.delete(message.id);
|
|
388
|
-
this.#sections.push(section);
|
|
389
|
-
if (cap !== void 0 && this.#sections.length > cap) {
|
|
390
|
-
const overflow = this.#sections.length - cap + 1;
|
|
391
|
-
const folded = this.#sections.slice(0, overflow);
|
|
392
|
-
try {
|
|
393
|
-
const merged = {
|
|
394
|
-
id: crypto.randomUUID(),
|
|
395
|
-
summary: await summarize(folded.map((one) => this.#summaryMessage(one))),
|
|
396
|
-
messages: folded.flatMap((one) => one.messages)
|
|
397
|
-
};
|
|
398
|
-
this.#sections.splice(0, overflow, merged);
|
|
399
|
-
this.#emitter.emit("collapse", merged);
|
|
400
|
-
} catch (error) {
|
|
401
|
-
this.#summary = await summarize(this.#sections.map((one) => this.#summaryMessage(one)));
|
|
402
|
-
this.#emitter.emit("summary", this.#summary);
|
|
403
|
-
throw error;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
this.#summary = await summarize(this.#sections.map((one) => this.#summaryMessage(one)));
|
|
407
|
-
this.#emitter.emit("summary", this.#summary);
|
|
408
|
-
this.#emitter.emit("compact", section);
|
|
409
|
-
return section;
|
|
410
|
-
}
|
|
411
|
-
rehydrate(id) {
|
|
412
|
-
const section = this.#sections.find((one) => one.id === id);
|
|
413
|
-
this.#emitter.emit("rehydrate", id);
|
|
414
|
-
return section === void 0 ? [] : section.messages;
|
|
415
|
-
}
|
|
416
|
-
search(query) {
|
|
417
|
-
const needle = query.toLowerCase();
|
|
418
|
-
return [...this.#sections.flatMap((section) => section.messages), ...this.#messages.values()].filter((message) => message.content.toLowerCase().includes(needle));
|
|
419
|
-
}
|
|
420
|
-
reference(options) {
|
|
421
|
-
const lines = [`[Reference — conversation "${options?.label ?? this.#id}" — NOT part of this conversation]`];
|
|
422
|
-
if (options?.summary !== false && this.#summary !== void 0) lines.push(`Summary: ${this.#summary}`);
|
|
423
|
-
const messages = options?.messages ?? [];
|
|
424
|
-
if (messages.length > 0) {
|
|
425
|
-
lines.push("Relevant messages:");
|
|
426
|
-
for (const message of messages) lines.push(`- ${message.role}: ${message.content}`);
|
|
427
|
-
}
|
|
428
|
-
return lines.join("\n");
|
|
429
|
-
}
|
|
430
|
-
snapshot() {
|
|
431
|
-
return {
|
|
432
|
-
id: this.#id,
|
|
433
|
-
...this.#summary === void 0 ? {} : { summary: this.#summary },
|
|
434
|
-
sections: this.sections,
|
|
435
|
-
messages: this.messages()
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
#summaryMessage(section) {
|
|
439
|
-
return {
|
|
440
|
-
id: section.id,
|
|
441
|
-
role: "assistant",
|
|
442
|
-
content: section.summary
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
#recapMessage(section) {
|
|
446
|
-
return {
|
|
447
|
-
id: section.id,
|
|
448
|
-
role: "assistant",
|
|
449
|
-
content: `${CONVERSATION_RECAP_PREFIX}${section.summary}`
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
#create(input) {
|
|
453
|
-
const message = {
|
|
454
|
-
id: crypto.randomUUID(),
|
|
455
|
-
role: input.role,
|
|
456
|
-
content: input.content,
|
|
457
|
-
...input.calls === void 0 ? {} : { calls: input.calls },
|
|
458
|
-
...input.images === void 0 ? {} : { images: input.images }
|
|
459
|
-
};
|
|
460
|
-
this.#messages.set(message.id, message);
|
|
461
|
-
return message;
|
|
462
|
-
}
|
|
463
|
-
};
|
|
464
|
-
//#endregion
|
|
465
|
-
//#region src/core/conversations/ConversationManager.ts
|
|
466
|
-
/**
|
|
467
|
-
* The registry of {@link Conversation}s keyed by `id`, in insertion order, WITH an active pointer
|
|
468
|
-
* — the §9 store over the conversation layer PLUS the `active` / `switch` seam the
|
|
469
|
-
* {@link import('../AgentContext.js').AgentContext} renders. Event-free (a registry, like
|
|
470
|
-
* {@link import('@orkestrel/workspace').WorkspaceManager}); the observability lives
|
|
471
|
-
* on each {@link Conversation}.
|
|
472
|
-
*
|
|
473
|
-
* @remarks
|
|
474
|
-
* - **Registry.** Conversations live in an insertion-ordered `Map` keyed by `id`. `add(input?)`
|
|
475
|
-
* mints a {@link Conversation} (its `id` from `input` or `crypto.randomUUID()`), flowing the
|
|
476
|
-
* manager's default `#summarize` / `#keep` in unless the `input` OVERRIDES them, and stores
|
|
477
|
-
* it (an already-present `id` OVERWRITES — last write wins). `count` is the map size,
|
|
478
|
-
* `conversation(id)` looks one up, `conversations()` lists them in insertion order.
|
|
479
|
-
* - **Active pointer.** `active` is the active conversation (the agent's message source the
|
|
480
|
-
* context renders), `undefined` until the FIRST `add` (which auto-activates it — a registry
|
|
481
|
-
* with conversations always has one active). A subsequent `add` leaves `active` unchanged.
|
|
482
|
-
* `switch(id)` re-points `active` to the conversation with `id` and returns it; an unknown `id`
|
|
483
|
-
* returns `undefined` and leaves `active` unchanged (the lenient lookup style — never throws,
|
|
484
|
-
* no new error code).
|
|
485
|
-
* - **Removal.** `remove` drops one by id, or a batch (§9.2) — `true` when any was removed;
|
|
486
|
-
* removing the ACTIVE conversation sets `active` to `undefined`. `clear` empties the registry
|
|
487
|
-
* and sets `active` to `undefined`.
|
|
488
|
-
* - **Event-free.** A purely registry store — no Emitter, no events (each conversation owns
|
|
489
|
-
* its own observable `emitter`).
|
|
490
|
-
*
|
|
491
|
-
* @example
|
|
492
|
-
* ```ts
|
|
493
|
-
* const manager = new ConversationManager({ summarize: async (m) => `recap of ${m.length}` })
|
|
494
|
-
* const conversation = manager.add() // auto-activates — active === conversation
|
|
495
|
-
* manager.add({ id: 'scratch' }) // leaves active unchanged
|
|
496
|
-
* manager.switch('scratch') // re-points active to the 'scratch' conversation
|
|
497
|
-
* manager.count // 2
|
|
498
|
-
* ```
|
|
499
|
-
*/
|
|
500
|
-
var ConversationManager = class {
|
|
501
|
-
#conversations = /* @__PURE__ */ new Map();
|
|
502
|
-
#active;
|
|
503
|
-
#summarize;
|
|
504
|
-
#keep;
|
|
505
|
-
#sections;
|
|
506
|
-
#store;
|
|
507
|
-
constructor(options) {
|
|
508
|
-
this.#summarize = options?.summarize;
|
|
509
|
-
this.#keep = options?.keep ?? 0;
|
|
510
|
-
this.#sections = options?.sections;
|
|
511
|
-
this.#store = options?.store;
|
|
512
|
-
}
|
|
513
|
-
get count() {
|
|
514
|
-
return this.#conversations.size;
|
|
515
|
-
}
|
|
516
|
-
get active() {
|
|
517
|
-
return this.#active === void 0 ? void 0 : this.#conversations.get(this.#active);
|
|
518
|
-
}
|
|
519
|
-
conversation(id) {
|
|
520
|
-
return this.#conversations.get(id);
|
|
521
|
-
}
|
|
522
|
-
conversations() {
|
|
523
|
-
return [...this.#conversations.values()];
|
|
524
|
-
}
|
|
525
|
-
add(input) {
|
|
526
|
-
const sections = input?.sections ?? this.#sections;
|
|
527
|
-
const summarize = input?.summarize ?? this.#summarize;
|
|
528
|
-
const conversation = new Conversation({
|
|
529
|
-
...input?.id === void 0 ? {} : { id: input.id },
|
|
530
|
-
...input?.on === void 0 ? {} : { on: input.on },
|
|
531
|
-
...summarize === void 0 ? {} : { summarize },
|
|
532
|
-
keep: input?.keep ?? this.#keep,
|
|
533
|
-
...sections === void 0 ? {} : { sections }
|
|
534
|
-
}, input?.snapshot);
|
|
535
|
-
this.#conversations.set(conversation.id, conversation);
|
|
536
|
-
if (this.#active === void 0) this.#active = conversation.id;
|
|
537
|
-
return conversation;
|
|
538
|
-
}
|
|
539
|
-
switch(id) {
|
|
540
|
-
const conversation = this.#conversations.get(id);
|
|
541
|
-
if (conversation === void 0) return void 0;
|
|
542
|
-
this.#active = id;
|
|
543
|
-
return conversation;
|
|
544
|
-
}
|
|
545
|
-
async open(id) {
|
|
546
|
-
const existing = this.#conversations.get(id);
|
|
547
|
-
if (existing !== void 0) {
|
|
548
|
-
this.#active = id;
|
|
549
|
-
return existing;
|
|
550
|
-
}
|
|
551
|
-
if (this.#store === void 0) return void 0;
|
|
552
|
-
const snapshot = await this.#store.get(id);
|
|
553
|
-
if (snapshot === void 0) return void 0;
|
|
554
|
-
const conversation = this.add({ snapshot });
|
|
555
|
-
this.#active = conversation.id;
|
|
556
|
-
return conversation;
|
|
557
|
-
}
|
|
558
|
-
async save(id) {
|
|
559
|
-
const conversation = this.#conversations.get(id);
|
|
560
|
-
if (this.#store === void 0 || conversation === void 0) return false;
|
|
561
|
-
await this.#store.set(conversation.snapshot());
|
|
562
|
-
return true;
|
|
563
|
-
}
|
|
564
|
-
remove(ids) {
|
|
565
|
-
if (isArray(ids)) {
|
|
566
|
-
let removed = false;
|
|
567
|
-
for (const id of ids) if (this.#drop(id)) removed = true;
|
|
568
|
-
return removed;
|
|
569
|
-
}
|
|
570
|
-
return this.#drop(ids);
|
|
571
|
-
}
|
|
572
|
-
clear() {
|
|
573
|
-
this.#conversations.clear();
|
|
574
|
-
this.#active = void 0;
|
|
575
|
-
}
|
|
576
|
-
#drop(id) {
|
|
577
|
-
const removed = this.#conversations.delete(id);
|
|
578
|
-
if (removed && this.#active === id) this.#active = void 0;
|
|
579
|
-
return removed;
|
|
580
|
-
}
|
|
581
|
-
};
|
|
582
|
-
//#endregion
|
|
583
267
|
//#region src/core/helpers.ts
|
|
584
268
|
/**
|
|
585
|
-
*
|
|
269
|
+
* Projects an unknown value onto the canonical JSON representation of an
|
|
586
270
|
* {@link AgentResult}.
|
|
587
271
|
*
|
|
588
272
|
* @remarks
|
|
@@ -637,7 +321,7 @@ function agentResultToJSON(value) {
|
|
|
637
321
|
return captured.success ? parseJSONValue(captured.value) : void 0;
|
|
638
322
|
}
|
|
639
323
|
/**
|
|
640
|
-
*
|
|
324
|
+
* Filters a list of items by a {@link import('./types.js').ScopeInterface} allow-list of
|
|
641
325
|
* keys — the pure, total set-membership primitive the context's build step and the agent
|
|
642
326
|
* loop's tool-advertise step apply a scope through.
|
|
643
327
|
*
|
|
@@ -654,7 +338,7 @@ function agentResultToJSON(value) {
|
|
|
654
338
|
* @typeParam T - The item type being filtered
|
|
655
339
|
* @param allow - The allow-list of keys (`undefined` ⇒ all, `[]` ⇒ none, else only-listed)
|
|
656
340
|
* @param items - The items to filter (returned unchanged when `allow` is `undefined`)
|
|
657
|
-
* @param key - Extracts the key an item is matched on (
|
|
341
|
+
* @param key - Extracts the key an item is matched on (for example an instruction's `name`)
|
|
658
342
|
* @returns The items that pass the allow-list, in their original order
|
|
659
343
|
*
|
|
660
344
|
* @example
|
|
@@ -672,7 +356,7 @@ function filterAllowList(allow, items, key) {
|
|
|
672
356
|
return items.filter((item) => set.has(key(item)));
|
|
673
357
|
}
|
|
674
358
|
/**
|
|
675
|
-
*
|
|
359
|
+
* Estimates the context-token footprint of a string — the deterministic char-based heuristic
|
|
676
360
|
* {@link estimateMessages} sums over a conversation's messages (the default context-budget
|
|
677
361
|
* estimator).
|
|
678
362
|
*
|
|
@@ -697,7 +381,7 @@ function estimateTokens(text) {
|
|
|
697
381
|
return Math.ceil(text.length / 4);
|
|
698
382
|
}
|
|
699
383
|
/**
|
|
700
|
-
*
|
|
384
|
+
* Estimates the context-token footprint of a batch of messages — the default `consumer`
|
|
701
385
|
* estimator for an agent's context `BudgetInterface` (a budgets surface's tracking contract)
|
|
702
386
|
* (the {@link import('./types.js').AgentOptions} `window`).
|
|
703
387
|
*
|
|
@@ -709,7 +393,7 @@ function estimateTokens(text) {
|
|
|
709
393
|
* deliberately-approximate per-image cost — a base64 length is NOT a token proxy). Deterministic
|
|
710
394
|
* and provider-free — the same messages always yield the same estimate, with an empty batch `0`.
|
|
711
395
|
* It is the fully-swappable default an agent's auto-compaction context budget charges each
|
|
712
|
-
* turn's new messages through; a caller wanting a sharper count supplies its own `
|
|
396
|
+
* turn's new messages through; a caller wanting a sharper count supplies its own `consumer` to
|
|
713
397
|
* `createBudget` instead. Total — never throws: a `calls` `JSON.stringify` that throws (a
|
|
714
398
|
* circular `ToolCall.arguments`) is caught and replaced with a conservative fixed contribution of
|
|
715
399
|
* {@link import('./constants.js').MESSAGE_TOKEN_OVERHEAD} (the same per-message overhead scale)
|
|
@@ -738,47 +422,48 @@ function estimateMessages(messages) {
|
|
|
738
422
|
}, 0);
|
|
739
423
|
}
|
|
740
424
|
/**
|
|
741
|
-
*
|
|
425
|
+
* Runs one rehydrated agent and applies the partial-as-configurable-failure policy — the
|
|
742
426
|
* shared job-handler step BOTH `createAgentQueue` and `createAgentRunner` settle each job
|
|
743
427
|
* through, so the policy can never diverge between them.
|
|
744
428
|
*
|
|
745
429
|
* @remarks
|
|
746
430
|
* A turn that committed PARTIAL (a cancel — abort / budget / timeout) is by default a
|
|
747
431
|
* FAILURE, so it THROWS an {@link import('./errors.js').AgentJobError} carrying the partial
|
|
748
|
-
* (the Queue's retries + a Runner's fail-fast then engage);
|
|
749
|
-
*
|
|
432
|
+
* (the Queue's retries + a Runner's fail-fast then engage); the `partial` policy RESOLVES
|
|
433
|
+
* it as success instead. A natural finish ALWAYS resolves with its result.
|
|
750
434
|
*
|
|
751
435
|
* @param agent - The rehydrated {@link AgentInterface} to run to its {@link AgentResult}
|
|
752
|
-
* @param
|
|
753
|
-
* (the default policy), a partial result throws an {@link AgentJobError}
|
|
754
|
-
* @returns The agent's {@link AgentResult} (a natural finish, or a partial
|
|
755
|
-
*
|
|
436
|
+
* @param partial - The partial policy. If `true`, a partial result resolves as success; if
|
|
437
|
+
* `false` (the default policy), a partial result throws an {@link AgentJobError}
|
|
438
|
+
* @returns The agent's {@link AgentResult} (a natural finish, or a partial one under the
|
|
439
|
+
* `partial` policy)
|
|
440
|
+
* @throws {AgentJobError} Thrown when the run ended partial and the `partial` policy is `false`
|
|
756
441
|
*
|
|
757
442
|
* @example
|
|
758
443
|
* ```ts
|
|
759
444
|
* const result = await settleAgentJob(registry.build(input, signal), false)
|
|
760
445
|
* ```
|
|
761
446
|
*/
|
|
762
|
-
async function settleAgentJob(agent,
|
|
447
|
+
async function settleAgentJob(agent, partial) {
|
|
763
448
|
const result = await agent.generate();
|
|
764
|
-
if (result.partial && !
|
|
449
|
+
if (result.partial && !partial) throw new AgentJobError("agent job ended partial", result);
|
|
765
450
|
return result;
|
|
766
451
|
}
|
|
767
452
|
/**
|
|
768
|
-
*
|
|
453
|
+
* Handles one queued agent job by rehydrating it through a registry with the queue
|
|
769
454
|
* attempt's signal, then applying the shared partial-result policy.
|
|
770
455
|
*
|
|
771
456
|
* @param registry - The registry that rehydrates the serializable job
|
|
772
|
-
* @param
|
|
457
|
+
* @param partial - The partial policy. If `true`, a partial result resolves; if `false`, it throws
|
|
773
458
|
* @param input - The serializable agent job
|
|
774
|
-
* @param
|
|
459
|
+
* @param context - The queue attempt whose signal bounds the agent
|
|
775
460
|
* @returns The settled agent result
|
|
776
461
|
*/
|
|
777
|
-
function handleAgentQueueJob(registry,
|
|
778
|
-
return settleAgentJob(registry.build(input,
|
|
462
|
+
function handleAgentQueueJob(registry, partial, input, context) {
|
|
463
|
+
return settleAgentJob(registry.build(input, context.signal), partial);
|
|
779
464
|
}
|
|
780
465
|
/**
|
|
781
|
-
*
|
|
466
|
+
* Handles one runner agent job by fanning out its declared children, rehydrating the
|
|
782
467
|
* parent through a registry with the controller signal, and applying the shared
|
|
783
468
|
* partial-result policy.
|
|
784
469
|
*
|
|
@@ -787,189 +472,779 @@ function handleAgentQueueJob(registry, allowPartial, input, execution) {
|
|
|
787
472
|
* inline, preserving bounded-runner progress.
|
|
788
473
|
*
|
|
789
474
|
* @param registry - The registry that rehydrates serializable jobs
|
|
790
|
-
* @param
|
|
475
|
+
* @param partial - The partial policy. If `true`, a partial result resolves; if `false`, it throws
|
|
791
476
|
* @param controller - The runner controller for this parent job
|
|
792
477
|
* @returns The settled parent agent result
|
|
793
478
|
*/
|
|
794
|
-
function handleAgentRunnerJob(registry,
|
|
479
|
+
function handleAgentRunnerJob(registry, partial, controller) {
|
|
795
480
|
const children = controller.input.children;
|
|
796
481
|
if (children !== void 0) for (const child of children) controller.spawn(child);
|
|
797
|
-
return settleAgentJob(registry.build(controller.input, controller.signal),
|
|
482
|
+
return settleAgentJob(registry.build(controller.input, controller.signal), partial);
|
|
798
483
|
}
|
|
799
484
|
/**
|
|
800
|
-
*
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
* {@link import('@orkestrel/workspace').isFile}.
|
|
485
|
+
* Renders a path-addressed text body as a fenced reference block — the framing an
|
|
486
|
+
* {@link import('./AgentContext.js').AgentContext}'s ACTIVE-workspace text-file render emits (the
|
|
487
|
+
* active workspace is the SOLE document/image context).
|
|
804
488
|
*
|
|
805
489
|
* @remarks
|
|
806
|
-
*
|
|
807
|
-
*
|
|
808
|
-
* —
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
*
|
|
812
|
-
*
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
*
|
|
816
|
-
*
|
|
490
|
+
* Produces `` File: <path>\n```<language>\n<content>\n``` `` — the `File:` label line, then a
|
|
491
|
+
* fenced code block tagged with `language`, the `content` verbatim inside. Pure string assembly,
|
|
492
|
+
* total — never throws. The one fenced-file format string for the whole module — `AgentContext.build()`
|
|
493
|
+
* frames an active workspace's text files with it (each carries its own `language` on its
|
|
494
|
+
* {@link import('@orkestrel/workspace').FileContent} text arm).
|
|
495
|
+
*
|
|
496
|
+
* @param path - The file path shown on the `File:` label line
|
|
497
|
+
* @param language - The fenced-code language tag (for example `'typescript'`)
|
|
498
|
+
* @param content - The file body rendered verbatim inside the fence
|
|
499
|
+
* @returns The fenced reference block
|
|
500
|
+
*
|
|
501
|
+
* @example
|
|
502
|
+
* ```ts
|
|
503
|
+
* import { renderFencedFile } from '@orkestrel/agent'
|
|
504
|
+
*
|
|
505
|
+
* renderFencedFile('src/main.ts', 'typescript', 'const x = 1')
|
|
506
|
+
* // 'File: src/main.ts\n```typescript\nconst x = 1\n```'
|
|
507
|
+
* ```
|
|
508
|
+
*/
|
|
509
|
+
function renderFencedFile(path, language, content) {
|
|
510
|
+
return `File: ${path}\n\`\`\`${language}\n${content}\n\`\`\``;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Sanitizes one reported token count into a safe non-negative integer.
|
|
514
|
+
*
|
|
515
|
+
* @param value - The token count to sanitize
|
|
516
|
+
* @returns The floored count, or `0` when the value is non-finite or non-positive
|
|
517
|
+
*/
|
|
518
|
+
function sanitizeToken(value) {
|
|
519
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Sanitizes a {@link TokenUsage} into safe, non-negative integers — the guard an agent's
|
|
523
|
+
* abort-usage path applies to a provider's partial usage before it is charged against a
|
|
524
|
+
* budget or folded into the run total.
|
|
525
|
+
*
|
|
526
|
+
* @remarks
|
|
527
|
+
* Per field (`prompt` / `completion` / `total`): a non-finite value (`NaN`, `+Infinity`,
|
|
528
|
+
* `-Infinity`) or a negative value floors to `0`; a fractional value floors to its
|
|
529
|
+
* non-negative integer part. No upper cap is applied. Total — never throws.
|
|
530
|
+
*
|
|
531
|
+
* @param usage - The token usage to sanitize (for example a provider's abort-partial usage)
|
|
532
|
+
* @returns A new {@link TokenUsage} with every field a safe non-negative integer
|
|
533
|
+
*
|
|
534
|
+
* @example
|
|
535
|
+
* ```ts
|
|
536
|
+
* sanitizeUsage({ prompt: -5, completion: NaN, total: 12.7 }) // { prompt: 0, completion: 0, total: 12 }
|
|
537
|
+
* ```
|
|
538
|
+
*/
|
|
539
|
+
function sanitizeUsage(usage) {
|
|
540
|
+
return {
|
|
541
|
+
prompt: sanitizeToken(usage.prompt),
|
|
542
|
+
completion: sanitizeToken(usage.completion),
|
|
543
|
+
total: sanitizeToken(usage.total)
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Joins the reasoning a run's provider calls separated from the answer — the first call
|
|
548
|
+
* seeds the accumulation, a later call appends blank-line separated so each turn's reasoning
|
|
549
|
+
* stays readable.
|
|
550
|
+
*
|
|
551
|
+
* @remarks
|
|
552
|
+
* Pure and total. `running` is `undefined` until a call surfaces reasoning, so the first join
|
|
553
|
+
* returns `next` verbatim (no leading separator). The result is display/audit metadata that
|
|
554
|
+
* never re-enters the conversation.
|
|
555
|
+
*
|
|
556
|
+
* @param running - The reasoning accumulated so far (`undefined` before the first)
|
|
557
|
+
* @param next - This call's separated reasoning
|
|
558
|
+
* @returns The joined reasoning
|
|
559
|
+
*
|
|
560
|
+
* @example
|
|
561
|
+
* ```ts
|
|
562
|
+
* joinThinking(undefined, 'first') // 'first'
|
|
563
|
+
* joinThinking('first', 'second') // 'first\n\nsecond'
|
|
564
|
+
* ```
|
|
565
|
+
*/
|
|
566
|
+
function joinThinking(running, next) {
|
|
567
|
+
return running === void 0 ? next : `${running}\n\n${next}`;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Adds two {@link TokenUsage} values field by field — the running total an agent run keeps
|
|
571
|
+
* across its provider calls.
|
|
572
|
+
*
|
|
573
|
+
* @remarks
|
|
574
|
+
* Pure and total: the first call seeds the total (`running` `undefined` returns `next`
|
|
575
|
+
* unchanged), later calls accumulate. No sanitization happens here — charge a provider's
|
|
576
|
+
* reported usage through {@link sanitizeUsage} first.
|
|
577
|
+
*
|
|
578
|
+
* @param running - The total so far (`undefined` before the first usage-bearing call)
|
|
579
|
+
* @param next - This call's reported usage
|
|
580
|
+
* @returns The summed usage
|
|
581
|
+
*
|
|
582
|
+
* @example
|
|
583
|
+
* ```ts
|
|
584
|
+
* sumUsage(undefined, { prompt: 2, completion: 1, total: 3 }) // { prompt: 2, completion: 1, total: 3 }
|
|
585
|
+
* sumUsage({ prompt: 2, completion: 1, total: 3 }, { prompt: 1, completion: 1, total: 2 })
|
|
586
|
+
* // { prompt: 3, completion: 2, total: 5 }
|
|
587
|
+
* ```
|
|
588
|
+
*/
|
|
589
|
+
function sumUsage(running, next) {
|
|
590
|
+
if (running === void 0) return next;
|
|
591
|
+
return {
|
|
592
|
+
prompt: running.prompt + next.prompt,
|
|
593
|
+
completion: running.completion + next.completion,
|
|
594
|
+
total: running.total + next.total
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Assembles the settled {@link AgentResult} from a run's {@link RunOutcome} — `thinking` and
|
|
599
|
+
* `usage` are carried only when the run surfaced them.
|
|
600
|
+
*
|
|
601
|
+
* @remarks
|
|
602
|
+
* Pure and total. An absent optional is OMITTED rather than stored as `undefined` (the
|
|
603
|
+
* present-when-given convention the message store follows), so a settled result JSON
|
|
604
|
+
* round-trips without an explicit `undefined` field. `exhausted` is loop bookkeeping and does
|
|
605
|
+
* not reach the public result — the `exhaust` event carries it instead.
|
|
606
|
+
*
|
|
607
|
+
* @param outcome - The run's settled outcome
|
|
608
|
+
* @returns The public {@link AgentResult}
|
|
609
|
+
*
|
|
610
|
+
* @example
|
|
611
|
+
* ```ts
|
|
612
|
+
* assembleResult({ content: 'hi', thinking: undefined, usage: undefined, partial: false, exhausted: false })
|
|
613
|
+
* // { content: 'hi', partial: false }
|
|
614
|
+
* ```
|
|
615
|
+
*/
|
|
616
|
+
function assembleResult(outcome) {
|
|
617
|
+
const result = {
|
|
618
|
+
content: outcome.content,
|
|
619
|
+
partial: outcome.partial
|
|
620
|
+
};
|
|
621
|
+
if (outcome.thinking !== void 0) result.thinking = outcome.thinking;
|
|
622
|
+
if (outcome.usage !== void 0) result.usage = outcome.usage;
|
|
623
|
+
return result;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Synthesizes the denial {@link ToolResult} an authority-blocked call is fed back with — the
|
|
627
|
+
* call's `id` / `name` keyed back, carrying a denial `error` instead of a value.
|
|
628
|
+
*
|
|
629
|
+
* @remarks
|
|
630
|
+
* Pure and total. The rule's `reason` is rendered as `denied: <reason>` when one was given,
|
|
631
|
+
* else the generic `denied by authority`. There is no `value`, so the agent loop feeds it back
|
|
632
|
+
* exactly like a tool error and the model can react to it.
|
|
633
|
+
*
|
|
634
|
+
* @param call - The denied {@link ToolCall}
|
|
635
|
+
* @param reason - The rule's explanation, or `undefined` for the generic denial
|
|
636
|
+
* @returns The failure-arm {@link ToolResult}
|
|
637
|
+
*
|
|
638
|
+
* @example
|
|
639
|
+
* ```ts
|
|
640
|
+
* denyCall({ id: '1', name: 'drop', arguments: {} }, 'read-only mode')
|
|
641
|
+
* // { success: false, id: '1', name: 'drop', error: 'denied: read-only mode' }
|
|
642
|
+
* ```
|
|
643
|
+
*/
|
|
644
|
+
function denyCall(call, reason) {
|
|
645
|
+
return {
|
|
646
|
+
success: false,
|
|
647
|
+
id: call.id,
|
|
648
|
+
name: call.name,
|
|
649
|
+
error: reason !== void 0 ? `denied: ${reason}` : "denied by authority"
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Renders one context section — the resolved `open`, each item's rendering, and the resolved
|
|
654
|
+
* `close` when one exists, blank-line joined.
|
|
655
|
+
*
|
|
656
|
+
* @remarks
|
|
657
|
+
* Pure and total. A section with NO items renders nothing (`undefined`), so an empty or fully
|
|
658
|
+
* scoped-out manager stays silent — its `open` / `close` never appear without items. `close`
|
|
659
|
+
* is the only optional slot: an unset one (there is no built-in close) drops the
|
|
660
|
+
* trailing line.
|
|
661
|
+
*
|
|
662
|
+
* @typeParam T - The section item being rendered
|
|
663
|
+
* @param open - The section's resolved leading text
|
|
664
|
+
* @param items - The already scope-filtered items
|
|
665
|
+
* @param render - Renders one item to its prompt text
|
|
666
|
+
* @param close - The section's resolved trailing text, or `undefined` for none
|
|
667
|
+
* @returns The rendered section, or `undefined` when there are no items
|
|
668
|
+
*
|
|
669
|
+
* @example
|
|
670
|
+
* ```ts
|
|
671
|
+
* renderSection('## Instructions', [{ content: 'Be terse.' }], (one) => one.content, undefined)
|
|
672
|
+
* // '## Instructions\n\nBe terse.'
|
|
673
|
+
* renderSection('<rules>', [], (one) => one.content, '</rules>') // undefined (no items)
|
|
674
|
+
* ```
|
|
675
|
+
*/
|
|
676
|
+
function renderSection(open, items, render, close) {
|
|
677
|
+
if (items.length === 0) return void 0;
|
|
678
|
+
const lines = [open, ...items.map(render)];
|
|
679
|
+
if (close !== void 0) lines.push(close);
|
|
680
|
+
return lines.join("\n\n");
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Resolves one section's OPEN text through the format cascade — manager-options override >
|
|
684
|
+
* provider default > built-in header.
|
|
685
|
+
*
|
|
686
|
+
* @remarks
|
|
687
|
+
* Pure and total. The leading text has NO per-item level. A manager's `open` already
|
|
688
|
+
* encapsulates `[options-override → built-in]`, so it is reached only when neither the
|
|
689
|
+
* override's `open` nor the provider's `open` applies — and there it IS the built-in header.
|
|
690
|
+
*
|
|
691
|
+
* @typeParam T - The section item the manager renders
|
|
692
|
+
* @param manager - The section source (its `format` override + its built-in `open`)
|
|
693
|
+
* @param provider - The provider-default framing for this section, or `undefined`
|
|
694
|
+
* @returns The section's leading text
|
|
695
|
+
*
|
|
696
|
+
* @example
|
|
697
|
+
* ```ts
|
|
698
|
+
* resolveOpen(instructions, undefined) // '## Instructions' (the built-in header)
|
|
699
|
+
* resolveOpen(instructions, { open: '<rules>' }) // '<rules>' (the provider default)
|
|
700
|
+
* ```
|
|
701
|
+
*/
|
|
702
|
+
function resolveOpen(manager, provider) {
|
|
703
|
+
return manager.format?.open ?? provider?.open ?? manager.open;
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Resolves one section's CLOSE text through the format cascade — manager-options override >
|
|
707
|
+
* provider default.
|
|
708
|
+
*
|
|
709
|
+
* @remarks
|
|
710
|
+
* Pure and total. There is NO built-in close, so a section with neither level set returns
|
|
711
|
+
* `undefined` and {@link renderSection} appends no closing line. Paired with
|
|
712
|
+
* {@link resolveOpen}, one level can WRAP the whole group.
|
|
713
|
+
*
|
|
714
|
+
* @typeParam T - The section item the manager renders
|
|
715
|
+
* @param manager - The section source (its `format` override)
|
|
716
|
+
* @param provider - The provider-default framing for this section, or `undefined`
|
|
717
|
+
* @returns The section's trailing text, or `undefined` when no level sets one
|
|
718
|
+
*
|
|
719
|
+
* @example
|
|
720
|
+
* ```ts
|
|
721
|
+
* resolveClose(instructions, undefined) // undefined (no built-in close)
|
|
722
|
+
* resolveClose(instructions, { close: '</rules>' }) // '</rules>'
|
|
723
|
+
* ```
|
|
724
|
+
*/
|
|
725
|
+
function resolveClose(manager, provider) {
|
|
726
|
+
return manager.format?.close ?? provider?.close;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Resolves ONE item's rendering through the format cascade — item override >
|
|
730
|
+
* manager-options override > provider default > built-in rendering.
|
|
731
|
+
*
|
|
732
|
+
* @remarks
|
|
733
|
+
* Pure and total. The item's own `override` is the most-specific level (a fully-rendered
|
|
734
|
+
* string for that item alone). A manager's `render(item)` already encapsulates
|
|
735
|
+
* `[options-override → built-in]`, so it is reached only when no higher level applies — and
|
|
736
|
+
* there it IS the built-in rendering.
|
|
737
|
+
*
|
|
738
|
+
* @typeParam T - The section item being rendered (it may carry its own `override`)
|
|
739
|
+
* @param manager - The section source (its `format` override + its built-in `render`)
|
|
740
|
+
* @param provider - The provider-default framing for this section, or `undefined`
|
|
741
|
+
* @param item - The item to render
|
|
742
|
+
* @returns The item's prompt text
|
|
743
|
+
*
|
|
744
|
+
* @example
|
|
745
|
+
* ```ts
|
|
746
|
+
* resolveItem(instructions, undefined, terse) // 'Be terse.' (the built-in rendering)
|
|
747
|
+
* resolveItem(instructions, { render: (one) => `<rule>${one.content}</rule>` }, terse)
|
|
748
|
+
* // '<rule>Be terse.</rule>'
|
|
749
|
+
* ```
|
|
750
|
+
*/
|
|
751
|
+
function resolveItem(manager, provider, item) {
|
|
752
|
+
return item.override ?? manager.format?.render?.(item) ?? provider?.render?.(item) ?? manager.render(item);
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Copies a message with image data merged onto its `images` — the message's own images
|
|
756
|
+
* first, then the attached data.
|
|
757
|
+
*
|
|
758
|
+
* @remarks
|
|
759
|
+
* Pure and total: the original message is NEVER mutated. `calls` is carried only when the
|
|
760
|
+
* source message has one (kept omitted otherwise, mirroring the store's present-when-given
|
|
761
|
+
* convention).
|
|
762
|
+
*
|
|
763
|
+
* @param message - The message to copy (left unchanged)
|
|
764
|
+
* @param data - The base64 image data to attach
|
|
765
|
+
* @returns A new message carrying the merged `images`
|
|
766
|
+
*
|
|
767
|
+
* @example
|
|
768
|
+
* ```ts
|
|
769
|
+
* attachImages({ id: '1', role: 'user', content: 'Describe' }, ['<payload>'])
|
|
770
|
+
* // { id: '1', role: 'user', content: 'Describe', images: ['<payload>'] }
|
|
771
|
+
* ```
|
|
772
|
+
*/
|
|
773
|
+
function attachImages(message, data) {
|
|
774
|
+
const images = [...message.images ?? [], ...data];
|
|
775
|
+
return message.calls === void 0 ? {
|
|
776
|
+
id: message.id,
|
|
777
|
+
role: message.role,
|
|
778
|
+
content: message.content,
|
|
779
|
+
images
|
|
780
|
+
} : {
|
|
781
|
+
id: message.id,
|
|
782
|
+
role: message.role,
|
|
783
|
+
content: message.content,
|
|
784
|
+
calls: message.calls,
|
|
785
|
+
images
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Attaches image data to a conversation's LAST user message — the turn a vision provider
|
|
790
|
+
* reads images off.
|
|
791
|
+
*
|
|
792
|
+
* @remarks
|
|
793
|
+
* Pure and total: the conversation and its messages are NEVER mutated, and the returned array
|
|
794
|
+
* replaces exactly the one target message with the copy {@link attachImages} builds. Empty
|
|
795
|
+
* data returns the conversation unchanged; a conversation with NO user message returns it
|
|
796
|
+
* unchanged too (there is nowhere to attach, and the images already rode the system block).
|
|
817
797
|
*
|
|
818
|
-
* @param
|
|
819
|
-
* @
|
|
798
|
+
* @param conversation - The messages to attach into (left unchanged)
|
|
799
|
+
* @param data - The base64 image data to attach
|
|
800
|
+
* @returns The conversation with its last user message replaced by the carrying copy
|
|
820
801
|
*
|
|
821
802
|
* @example
|
|
822
803
|
* ```ts
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
* isMessage({ id: '1', role: 'user' }) // false (missing content)
|
|
826
|
-
* isMessage({ id: '1', role: 'assistant', content: '', calls: [null] }) // false (malformed call)
|
|
804
|
+
* attachUserImages([{ id: '1', role: 'user', content: 'Describe' }], ['<payload>'])
|
|
805
|
+
* // [{ id: '1', role: 'user', content: 'Describe', images: ['<payload>'] }]
|
|
827
806
|
* ```
|
|
828
807
|
*/
|
|
829
|
-
function
|
|
830
|
-
if (
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
808
|
+
function attachUserImages(conversation, data) {
|
|
809
|
+
if (data.length === 0) return conversation;
|
|
810
|
+
let target = -1;
|
|
811
|
+
for (let index = conversation.length - 1; index >= 0; index -= 1) if (conversation[index]?.role === "user") {
|
|
812
|
+
target = index;
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
if (target === -1) return conversation;
|
|
816
|
+
return conversation.map((message, index) => index === target ? attachImages(message, data) : message);
|
|
834
817
|
}
|
|
835
818
|
/**
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
* read via a guard, never an `as`).
|
|
819
|
+
* Collects the `base64` payload of the IMAGE files in a workspace file list — the data an
|
|
820
|
+
* agent context attaches to the last user message.
|
|
839
821
|
*
|
|
840
822
|
* @remarks
|
|
841
|
-
*
|
|
842
|
-
*
|
|
843
|
-
*
|
|
844
|
-
* the {@link SectionInterface} type at a storage boundary WITHOUT a cast.
|
|
823
|
+
* Pure and total. `isBinary` NARROWS the tagless content to its binary arm (a total guard,
|
|
824
|
+
* never an assertion), then the MIME prefix gates it to an image, so a text file and a non-image
|
|
825
|
+
* binary (a PDF) are both skipped. Order follows the file list.
|
|
845
826
|
*
|
|
846
|
-
* @param
|
|
847
|
-
* @returns
|
|
827
|
+
* @param files - The (already scope-filtered) workspace files
|
|
828
|
+
* @returns The `base64` payload of each image file, in file order
|
|
848
829
|
*
|
|
849
830
|
* @example
|
|
850
831
|
* ```ts
|
|
851
|
-
*
|
|
852
|
-
*
|
|
853
|
-
* isSection({ id: 's', messages: [] }) // false (missing summary)
|
|
832
|
+
* collectImageData([createFile({ path: 'a.png', content: { base64: '<payload>', mime: 'image/png' } })])
|
|
833
|
+
* // ['<payload>']
|
|
854
834
|
* ```
|
|
855
835
|
*/
|
|
856
|
-
function
|
|
857
|
-
|
|
858
|
-
if (
|
|
859
|
-
return
|
|
836
|
+
function collectImageData(files) {
|
|
837
|
+
const data = [];
|
|
838
|
+
for (const file of files) if (isBinary(file.content) && file.content.mime.startsWith("image/")) data.push(file.content.base64);
|
|
839
|
+
return data;
|
|
860
840
|
}
|
|
861
841
|
/**
|
|
862
|
-
*
|
|
863
|
-
*
|
|
864
|
-
* {@link import('./conversations/stores/DatabaseConversationStore.js').DatabaseConversationStore}
|
|
865
|
-
* reads back from its opaque JSON column, a snapshot loaded from disk). The EXACT analogue of
|
|
866
|
-
* {@link import('@orkestrel/workspace').isWorkspaceSnapshot}.
|
|
842
|
+
* Builds the RAW synthetic summary message for one compacted section — role `'assistant'`,
|
|
843
|
+
* the section's stable `id`, its `summary` VERBATIM as content.
|
|
867
844
|
*
|
|
868
845
|
* @remarks
|
|
869
|
-
*
|
|
870
|
-
*
|
|
871
|
-
*
|
|
872
|
-
* {@link
|
|
873
|
-
* valid {@link MessageInterface} ({@link isMessage}) — enough to safely impose the
|
|
874
|
-
* {@link ConversationSnapshot} type at a storage boundary WITHOUT a cast. The structural twin of
|
|
875
|
-
* {@link import('@orkestrel/workspace').isWorkspaceSnapshot}. A malformed blob (a non-record, a missing / non-string `id`, a
|
|
876
|
-
* non-string `summary` when present, a non-array `sections` / `messages`, or any malformed
|
|
877
|
-
* element) resolves `false`, so a
|
|
878
|
-
* {@link import('./conversations/stores/DatabaseConversationStore.js').DatabaseConversationStore}
|
|
879
|
-
* read yields `undefined` rather than a broken conversation.
|
|
846
|
+
* Pure and total. This is the unframed form the rollup regeneration digests (a
|
|
847
|
+
* summary-of-summaries over the section summaries); the recap LABEL is a `view()`
|
|
848
|
+
* presentation concern kept out of what the summarizer re-reads — see
|
|
849
|
+
* {@link buildRecapMessage}.
|
|
880
850
|
*
|
|
881
|
-
* @param
|
|
882
|
-
* @returns
|
|
851
|
+
* @param section - The compacted section to render
|
|
852
|
+
* @returns The synthetic summary message
|
|
883
853
|
*
|
|
884
854
|
* @example
|
|
885
855
|
* ```ts
|
|
886
|
-
*
|
|
887
|
-
*
|
|
888
|
-
* isConversationSnapshot({ id: 'c1', sections: 'nope', messages: [] }) // false
|
|
889
|
-
* isConversationSnapshot({ sections: [], messages: [] }) // false (missing id)
|
|
856
|
+
* buildSummaryMessage({ id: 's1', summary: 'recap', messages: [] })
|
|
857
|
+
* // { id: 's1', role: 'assistant', content: 'recap' }
|
|
890
858
|
* ```
|
|
891
859
|
*/
|
|
892
|
-
function
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
860
|
+
function buildSummaryMessage(section) {
|
|
861
|
+
return {
|
|
862
|
+
id: section.id,
|
|
863
|
+
role: "assistant",
|
|
864
|
+
content: section.summary
|
|
865
|
+
};
|
|
898
866
|
}
|
|
899
867
|
/**
|
|
900
|
-
*
|
|
901
|
-
* {@link
|
|
902
|
-
*
|
|
868
|
+
* Builds the FRAMED recap message for one compacted section — the same role and stable `id`
|
|
869
|
+
* as {@link buildSummaryMessage}, with the content prefixed by
|
|
870
|
+
* {@link import('./constants.js').CONVERSATION_RECAP_PREFIX}.
|
|
903
871
|
*
|
|
904
872
|
* @remarks
|
|
905
|
-
*
|
|
906
|
-
*
|
|
907
|
-
*
|
|
908
|
-
*
|
|
909
|
-
* {@link import('@orkestrel/workspace').FileContent} text arm).
|
|
873
|
+
* Pure and total. The prefix is what makes a small model read the message as a CONDENSED
|
|
874
|
+
* RECAP of earlier turns rather than a literal assistant turn to echo or answer from. It is a
|
|
875
|
+
* fixed handful of tokens, so a conversation's `view()` stays lean however many sections it
|
|
876
|
+
* carries.
|
|
910
877
|
*
|
|
911
|
-
* @param
|
|
912
|
-
* @
|
|
913
|
-
* @param content - The file body rendered verbatim inside the fence
|
|
914
|
-
* @returns The fenced reference block
|
|
878
|
+
* @param section - The compacted section to render
|
|
879
|
+
* @returns The framed recap message
|
|
915
880
|
*
|
|
916
881
|
* @example
|
|
917
882
|
* ```ts
|
|
918
|
-
*
|
|
919
|
-
*
|
|
920
|
-
* fencedFile('src/main.ts', 'typescript', 'const x = 1')
|
|
921
|
-
* // 'File: src/main.ts\n```typescript\nconst x = 1\n```'
|
|
883
|
+
* buildRecapMessage({ id: 's1', summary: 'recap', messages: [] })
|
|
884
|
+
* // { id: 's1', role: 'assistant', content: `${CONVERSATION_RECAP_PREFIX}recap` }
|
|
922
885
|
* ```
|
|
923
886
|
*/
|
|
924
|
-
function
|
|
925
|
-
return
|
|
887
|
+
function buildRecapMessage(section) {
|
|
888
|
+
return {
|
|
889
|
+
id: section.id,
|
|
890
|
+
role: "assistant",
|
|
891
|
+
content: `${CONVERSATION_RECAP_PREFIX}${section.summary}`
|
|
892
|
+
};
|
|
926
893
|
}
|
|
927
894
|
/**
|
|
928
|
-
*
|
|
895
|
+
* Intersects two scope category lists under the "`undefined` is the universal set" rule — the
|
|
896
|
+
* primitive a scope narrows through.
|
|
929
897
|
*
|
|
930
|
-
* @
|
|
931
|
-
*
|
|
898
|
+
* @remarks
|
|
899
|
+
* Pure and total, and it can only TIGHTEN: `undefined` ∩ `undefined` is `undefined` (still no
|
|
900
|
+
* constraint); `undefined` ∩ a list is a COPY of that list (the `undefined` side imposes
|
|
901
|
+
* nothing); a list ∩ a list keeps the child keys the parent also allows, so a parent-excluded
|
|
902
|
+
* key can never be re-admitted. Every returned list is a fresh copy, so a later mutation of
|
|
903
|
+
* either input cannot leak into the result.
|
|
904
|
+
*
|
|
905
|
+
* @param parent - The parent's allow-list (`undefined` ⇒ no constraint)
|
|
906
|
+
* @param child - The narrowing allow-list (`undefined` ⇒ no constraint)
|
|
907
|
+
* @returns The intersected allow-list, or `undefined` when neither side constrains
|
|
908
|
+
*
|
|
909
|
+
* @example
|
|
910
|
+
* ```ts
|
|
911
|
+
* intersectKeys(['read', 'write'], ['write', 'admin']) // ['write']
|
|
912
|
+
* intersectKeys(undefined, ['read']) // ['read']
|
|
913
|
+
* intersectKeys(undefined, undefined) // undefined
|
|
914
|
+
* ```
|
|
932
915
|
*/
|
|
933
|
-
function
|
|
934
|
-
|
|
916
|
+
function intersectKeys(parent, child) {
|
|
917
|
+
if (parent === void 0) return child === void 0 ? void 0 : [...child];
|
|
918
|
+
if (child === void 0) return [...parent];
|
|
919
|
+
const allowed = new Set(parent);
|
|
920
|
+
return child.filter((key) => allowed.has(key));
|
|
935
921
|
}
|
|
922
|
+
//#endregion
|
|
923
|
+
//#region src/core/conversations/Conversation.ts
|
|
936
924
|
/**
|
|
937
|
-
*
|
|
938
|
-
*
|
|
939
|
-
*
|
|
925
|
+
* Represents a conversation — a live uncompacted tail of messages it OWNS DIRECTLY ABOVE a
|
|
926
|
+
* flat message store, plus compacted, summarized {@link Section}s and a regenerated rollup
|
|
927
|
+
* `summary`, with on-demand `rehydrate` and substring `search`, driven by a provider-agnostic
|
|
928
|
+
* {@link ConversationSummaryHandler} seam (so `core` never imports a provider).
|
|
940
929
|
*
|
|
941
930
|
* @remarks
|
|
942
|
-
*
|
|
943
|
-
*
|
|
944
|
-
*
|
|
931
|
+
* - **Live tail + sections.** The conversation OWNS its live tail DIRECTLY — `#messages` is an
|
|
932
|
+
* insertion-ordered `Map` of immutable {@link Message}s keyed by their minted id
|
|
933
|
+
* (the SAME store mechanics a flat manager had, folded in: `add` / `message` / `messages` /
|
|
934
|
+
* `remove` / `clear` / `count`), exactly as a `Workspace` owns its files (no separate
|
|
935
|
+
* per-value manager). `#sections` are the compacted history (oldest → newest), each a
|
|
936
|
+
* summarized slice that RETAINS its originals. `#summary` is the rollup (a
|
|
937
|
+
* summary-of-summaries over all sections), regenerated on each compaction (`undefined`
|
|
938
|
+
* until the first).
|
|
939
|
+
* - **`view()`.** Each section folds to ONE synthetic summary message (role `'assistant'` — a
|
|
940
|
+
* prior-context recap — keyed by the section's stable `id`), then the live messages
|
|
941
|
+
* verbatim. The rollup `summary` is NOT injected (it is separately pull-able); `view()`
|
|
942
|
+
* carries the per-section summaries, which ARE the compaction benefit.
|
|
943
|
+
* - **`compact()`.** Folds the oldest `count - keep` live messages into a new section
|
|
944
|
+
* (its `summary` from `#summarize`), removes them from the live tail by id, regenerates the
|
|
945
|
+
* rollup (a SECOND `#summarize` over all section summaries), and emits `summary` then
|
|
946
|
+
* `compact`. Returns the section, or `undefined` when nothing folds (`count <= keep`).
|
|
947
|
+
* THROWS a {@link ConversationError} when no `#summarize` was supplied. Two summarizer calls
|
|
948
|
+
* per compaction.
|
|
949
|
+
* - **`rehydrate(id)` / `search(query)`.** `rehydrate` returns a section's full original
|
|
950
|
+
* messages (`[]` for an unknown id) and emits `rehydrate` — a pure read (the caller decides
|
|
951
|
+
* whether to re-add them; `rehydrate` never reinserts). `search` is a case-insensitive
|
|
952
|
+
* substring scan of `content` across ALL messages (every section's originals + the live tail).
|
|
953
|
+
* - **Observable.** The owned {@link emitter} ({@link ConversationEventMap}) carries
|
|
954
|
+
* `compact` / `summary` / `rehydrate`, emitted directly, strictly AFTER the state change;
|
|
955
|
+
* the emitter isolates a listener throw and routes it to its `error` handler (the `error`
|
|
956
|
+
* option), so a buggy observer can never corrupt a compaction.
|
|
945
957
|
*
|
|
946
|
-
* @
|
|
947
|
-
*
|
|
958
|
+
* @example
|
|
959
|
+
* ```ts
|
|
960
|
+
* const conversation = new Conversation({ summarize: async (m) => `recap of ${m.length}` })
|
|
961
|
+
* conversation.add([
|
|
962
|
+
* { role: 'user', content: 'Hello' },
|
|
963
|
+
* { role: 'assistant', content: 'Hi there' },
|
|
964
|
+
* ])
|
|
965
|
+
* const section = await conversation.compact() // folds both into one summarized section
|
|
966
|
+
* conversation.view() // [{ role: 'assistant', content: 'recap of 2' }] — the live tail is empty
|
|
967
|
+
* conversation.summary // 'recap of 1' — the rollup over the one section
|
|
968
|
+
* ```
|
|
969
|
+
*/
|
|
970
|
+
var Conversation = class {
|
|
971
|
+
#id;
|
|
972
|
+
#emitter;
|
|
973
|
+
#summarize;
|
|
974
|
+
#keep;
|
|
975
|
+
#cap;
|
|
976
|
+
#sections = [];
|
|
977
|
+
#summary;
|
|
978
|
+
#messages = /* @__PURE__ */ new Map();
|
|
979
|
+
constructor(options) {
|
|
980
|
+
const snapshot = options?.snapshot;
|
|
981
|
+
this.#id = snapshot?.id ?? options?.id ?? crypto.randomUUID();
|
|
982
|
+
this.#emitter = new Emitter({
|
|
983
|
+
...options?.on === void 0 ? {} : { on: options.on },
|
|
984
|
+
...options?.error === void 0 ? {} : { error: options.error }
|
|
985
|
+
});
|
|
986
|
+
this.#summarize = options?.summarize;
|
|
987
|
+
this.#keep = options?.keep ?? 0;
|
|
988
|
+
if (options?.sections !== void 0 && options.sections < 1) throw new ConversationError("SECTIONS", "a sections cap must be >= 1");
|
|
989
|
+
this.#cap = options?.sections;
|
|
990
|
+
if (snapshot !== void 0) {
|
|
991
|
+
this.#summary = snapshot.summary;
|
|
992
|
+
for (const section of snapshot.sections) this.#sections.push(section);
|
|
993
|
+
for (const message of snapshot.messages) this.#messages.set(message.id, message);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
get id() {
|
|
997
|
+
return this.#id;
|
|
998
|
+
}
|
|
999
|
+
get emitter() {
|
|
1000
|
+
return this.#emitter;
|
|
1001
|
+
}
|
|
1002
|
+
get summary() {
|
|
1003
|
+
return this.#summary;
|
|
1004
|
+
}
|
|
1005
|
+
get sections() {
|
|
1006
|
+
return [...this.#sections];
|
|
1007
|
+
}
|
|
1008
|
+
get summarizable() {
|
|
1009
|
+
return this.#summarize !== void 0;
|
|
1010
|
+
}
|
|
1011
|
+
get count() {
|
|
1012
|
+
return this.#messages.size;
|
|
1013
|
+
}
|
|
1014
|
+
add(input) {
|
|
1015
|
+
if (isArray(input)) return input.map((one) => this.#create(one));
|
|
1016
|
+
return this.#create(input);
|
|
1017
|
+
}
|
|
1018
|
+
message(id) {
|
|
1019
|
+
return this.#messages.get(id);
|
|
1020
|
+
}
|
|
1021
|
+
messages() {
|
|
1022
|
+
return [...this.#messages.values()];
|
|
1023
|
+
}
|
|
1024
|
+
remove(ids) {
|
|
1025
|
+
if (isArray(ids)) {
|
|
1026
|
+
let removed = true;
|
|
1027
|
+
for (const id of ids) if (!this.#messages.delete(id)) removed = false;
|
|
1028
|
+
return removed;
|
|
1029
|
+
}
|
|
1030
|
+
return this.#messages.delete(ids);
|
|
1031
|
+
}
|
|
1032
|
+
clear() {
|
|
1033
|
+
this.#messages.clear();
|
|
1034
|
+
}
|
|
1035
|
+
view() {
|
|
1036
|
+
return [...this.#sections.map((section) => buildRecapMessage(section)), ...this.#messages.values()];
|
|
1037
|
+
}
|
|
1038
|
+
async compact(options) {
|
|
1039
|
+
const summarize = this.#summarize;
|
|
1040
|
+
if (summarize === void 0) throw new ConversationError("SUMMARIZER", "cannot compact a conversation without a summarizer");
|
|
1041
|
+
const cap = options?.sections ?? this.#cap;
|
|
1042
|
+
if (cap !== void 0 && cap < 1) throw new ConversationError("SECTIONS", "a sections cap must be >= 1");
|
|
1043
|
+
const keep = options?.keep ?? this.#keep;
|
|
1044
|
+
const live = [...this.#messages.values()];
|
|
1045
|
+
const fold = keep <= 0 ? live.length : live.length - keep;
|
|
1046
|
+
if (fold <= 0) return void 0;
|
|
1047
|
+
const slice = live.slice(0, fold);
|
|
1048
|
+
const summary = await summarize(slice);
|
|
1049
|
+
const section = {
|
|
1050
|
+
id: crypto.randomUUID(),
|
|
1051
|
+
summary,
|
|
1052
|
+
messages: slice
|
|
1053
|
+
};
|
|
1054
|
+
for (const message of slice) this.#messages.delete(message.id);
|
|
1055
|
+
this.#sections.push(section);
|
|
1056
|
+
if (cap !== void 0 && this.#sections.length > cap) {
|
|
1057
|
+
const overflow = this.#sections.length - cap + 1;
|
|
1058
|
+
const folded = this.#sections.slice(0, overflow);
|
|
1059
|
+
try {
|
|
1060
|
+
const merged = {
|
|
1061
|
+
id: crypto.randomUUID(),
|
|
1062
|
+
summary: await summarize(folded.map((one) => buildSummaryMessage(one))),
|
|
1063
|
+
messages: folded.flatMap((one) => one.messages)
|
|
1064
|
+
};
|
|
1065
|
+
this.#sections.splice(0, overflow, merged);
|
|
1066
|
+
this.#emitter.emit("collapse", merged);
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
this.#summary = await summarize(this.#sections.map((one) => buildSummaryMessage(one)));
|
|
1069
|
+
this.#emitter.emit("summary", this.#summary);
|
|
1070
|
+
throw error;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
this.#summary = await summarize(this.#sections.map((one) => buildSummaryMessage(one)));
|
|
1074
|
+
this.#emitter.emit("summary", this.#summary);
|
|
1075
|
+
this.#emitter.emit("compact", section);
|
|
1076
|
+
return section;
|
|
1077
|
+
}
|
|
1078
|
+
rehydrate(id) {
|
|
1079
|
+
const section = this.#sections.find((one) => one.id === id);
|
|
1080
|
+
this.#emitter.emit("rehydrate", id);
|
|
1081
|
+
return section === void 0 ? [] : section.messages;
|
|
1082
|
+
}
|
|
1083
|
+
search(query) {
|
|
1084
|
+
const needle = query.toLowerCase();
|
|
1085
|
+
return [...this.#sections.flatMap((section) => section.messages), ...this.#messages.values()].filter((message) => message.content.toLowerCase().includes(needle));
|
|
1086
|
+
}
|
|
1087
|
+
reference(options) {
|
|
1088
|
+
const lines = [`[Reference — conversation "${options?.label ?? this.#id}" — NOT part of this conversation]`];
|
|
1089
|
+
if (options?.summary !== false && this.#summary !== void 0) lines.push(`Summary: ${this.#summary}`);
|
|
1090
|
+
const messages = options?.messages ?? [];
|
|
1091
|
+
if (messages.length > 0) {
|
|
1092
|
+
lines.push("Relevant messages:");
|
|
1093
|
+
for (const message of messages) lines.push(`- ${message.role}: ${message.content}`);
|
|
1094
|
+
}
|
|
1095
|
+
return lines.join("\n");
|
|
1096
|
+
}
|
|
1097
|
+
snapshot() {
|
|
1098
|
+
return {
|
|
1099
|
+
id: this.#id,
|
|
1100
|
+
...this.#summary === void 0 ? {} : { summary: this.#summary },
|
|
1101
|
+
sections: this.sections,
|
|
1102
|
+
messages: this.messages()
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
#create(input) {
|
|
1106
|
+
const message = {
|
|
1107
|
+
id: crypto.randomUUID(),
|
|
1108
|
+
role: input.role,
|
|
1109
|
+
content: input.content,
|
|
1110
|
+
...input.calls === void 0 ? {} : { calls: input.calls },
|
|
1111
|
+
...input.images === void 0 ? {} : { images: input.images }
|
|
1112
|
+
};
|
|
1113
|
+
this.#messages.set(message.id, message);
|
|
1114
|
+
return message;
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
//#endregion
|
|
1118
|
+
//#region src/core/conversations/ConversationManager.ts
|
|
1119
|
+
/**
|
|
1120
|
+
* Registers {@link Conversation}s keyed by `id`, in insertion order, WITH an active pointer
|
|
1121
|
+
* — the id-keyed store over the conversation layer PLUS the `active` / `switch` seam the
|
|
1122
|
+
* {@link import('../AgentContext.js').AgentContext} renders. Event-free (a registry, like
|
|
1123
|
+
* {@link import('@orkestrel/workspace').WorkspaceManager}); the observability lives
|
|
1124
|
+
* on each {@link Conversation}.
|
|
1125
|
+
*
|
|
1126
|
+
* @remarks
|
|
1127
|
+
* - **Registry.** Conversations live in an insertion-ordered `Map` keyed by `id`. `add(input?)`
|
|
1128
|
+
* mints a {@link Conversation} (its `id` from `input` or `crypto.randomUUID()`), flowing the
|
|
1129
|
+
* manager's default `#summarize` / `#keep` in unless the `input` OVERRIDES them, and stores
|
|
1130
|
+
* it (an already-present `id` OVERWRITES — last write wins). `count` is the map size,
|
|
1131
|
+
* `conversation(id)` looks one up, `conversations()` lists them in insertion order.
|
|
1132
|
+
* - **Active pointer.** `active` is the active conversation (the agent's message source the
|
|
1133
|
+
* context renders), `undefined` until the FIRST `add` (which auto-activates it — a registry
|
|
1134
|
+
* with conversations always has one active). A subsequent `add` leaves `active` unchanged.
|
|
1135
|
+
* `switch(id)` re-points `active` to the conversation with `id` and returns it; an unknown `id`
|
|
1136
|
+
* returns `undefined` and leaves `active` unchanged (the lenient lookup style — never throws,
|
|
1137
|
+
* no new error code).
|
|
1138
|
+
* - **Removal.** `remove` drops one by id, or a batch — `true` only when EVERY supplied id was
|
|
1139
|
+
* removed; removing the ACTIVE conversation sets `active` to `undefined`. `clear` empties the registry
|
|
1140
|
+
* and sets `active` to `undefined`.
|
|
1141
|
+
* - **Event-free.** A purely registry store — no Emitter, no events (each conversation owns
|
|
1142
|
+
* its own observable `emitter`).
|
|
948
1143
|
*
|
|
949
1144
|
* @example
|
|
950
1145
|
* ```ts
|
|
951
|
-
*
|
|
1146
|
+
* const manager = new ConversationManager({ summarize: async (m) => `recap of ${m.length}` })
|
|
1147
|
+
* const conversation = manager.add() // auto-activates — active === conversation
|
|
1148
|
+
* manager.add({ id: 'scratch' }) // leaves active unchanged
|
|
1149
|
+
* manager.switch('scratch') // re-points active to the 'scratch' conversation
|
|
1150
|
+
* manager.count // 2
|
|
952
1151
|
* ```
|
|
953
1152
|
*/
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1153
|
+
var ConversationManager = class {
|
|
1154
|
+
#conversations = /* @__PURE__ */ new Map();
|
|
1155
|
+
#active;
|
|
1156
|
+
#summarize;
|
|
1157
|
+
#keep;
|
|
1158
|
+
#sections;
|
|
1159
|
+
#store;
|
|
1160
|
+
constructor(options) {
|
|
1161
|
+
this.#summarize = options?.summarize;
|
|
1162
|
+
this.#keep = options?.keep ?? 0;
|
|
1163
|
+
this.#sections = options?.sections;
|
|
1164
|
+
this.#store = options?.store;
|
|
1165
|
+
}
|
|
1166
|
+
get count() {
|
|
1167
|
+
return this.#conversations.size;
|
|
1168
|
+
}
|
|
1169
|
+
get active() {
|
|
1170
|
+
return this.#active === void 0 ? void 0 : this.#conversations.get(this.#active);
|
|
1171
|
+
}
|
|
1172
|
+
conversation(id) {
|
|
1173
|
+
return this.#conversations.get(id);
|
|
1174
|
+
}
|
|
1175
|
+
conversations() {
|
|
1176
|
+
return [...this.#conversations.values()];
|
|
1177
|
+
}
|
|
1178
|
+
add(input) {
|
|
1179
|
+
const sections = input?.sections ?? this.#sections;
|
|
1180
|
+
const summarize = input?.summarize ?? this.#summarize;
|
|
1181
|
+
const conversation = new Conversation({
|
|
1182
|
+
...input?.id === void 0 ? {} : { id: input.id },
|
|
1183
|
+
...input?.on === void 0 ? {} : { on: input.on },
|
|
1184
|
+
...summarize === void 0 ? {} : { summarize },
|
|
1185
|
+
keep: input?.keep ?? this.#keep,
|
|
1186
|
+
...sections === void 0 ? {} : { sections },
|
|
1187
|
+
...input?.snapshot === void 0 ? {} : { snapshot: input.snapshot }
|
|
1188
|
+
});
|
|
1189
|
+
this.#conversations.set(conversation.id, conversation);
|
|
1190
|
+
if (this.#active === void 0) this.#active = conversation.id;
|
|
1191
|
+
return conversation;
|
|
1192
|
+
}
|
|
1193
|
+
switch(id) {
|
|
1194
|
+
const conversation = this.#conversations.get(id);
|
|
1195
|
+
if (conversation === void 0) return void 0;
|
|
1196
|
+
this.#active = id;
|
|
1197
|
+
return conversation;
|
|
1198
|
+
}
|
|
1199
|
+
async open(id) {
|
|
1200
|
+
const existing = this.#conversations.get(id);
|
|
1201
|
+
if (existing !== void 0) {
|
|
1202
|
+
this.#active = id;
|
|
1203
|
+
return existing;
|
|
1204
|
+
}
|
|
1205
|
+
if (this.#store === void 0) return void 0;
|
|
1206
|
+
const snapshot = await this.#store.get(id);
|
|
1207
|
+
if (snapshot === void 0) return void 0;
|
|
1208
|
+
const conversation = this.add({ snapshot });
|
|
1209
|
+
this.#active = conversation.id;
|
|
1210
|
+
return conversation;
|
|
1211
|
+
}
|
|
1212
|
+
async save(id) {
|
|
1213
|
+
const conversation = this.#conversations.get(id);
|
|
1214
|
+
if (this.#store === void 0 || conversation === void 0) return false;
|
|
1215
|
+
await this.#store.set(conversation.snapshot());
|
|
1216
|
+
return true;
|
|
1217
|
+
}
|
|
1218
|
+
remove(ids) {
|
|
1219
|
+
if (isArray(ids)) {
|
|
1220
|
+
let removed = true;
|
|
1221
|
+
for (const id of ids) if (!this.#drop(id)) removed = false;
|
|
1222
|
+
return removed;
|
|
1223
|
+
}
|
|
1224
|
+
return this.#drop(ids);
|
|
1225
|
+
}
|
|
1226
|
+
clear() {
|
|
1227
|
+
this.#conversations.clear();
|
|
1228
|
+
this.#active = void 0;
|
|
1229
|
+
}
|
|
1230
|
+
#drop(id) {
|
|
1231
|
+
const removed = this.#conversations.delete(id);
|
|
1232
|
+
if (removed && this.#active === id) this.#active = void 0;
|
|
1233
|
+
return removed;
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
961
1236
|
//#endregion
|
|
962
1237
|
//#region src/core/instructions/Instruction.ts
|
|
963
1238
|
/**
|
|
964
|
-
*
|
|
965
|
-
* input (`name` / `content`, an optional `priority` defaulting to `0`), the `id`
|
|
966
|
-
* at construction.
|
|
1239
|
+
* Represents an immutable named directive — a {@link InstructionInterface} assembled once
|
|
1240
|
+
* from its input (`name` / `content`, an optional `priority` defaulting to `0`), the `id`
|
|
1241
|
+
* minted at construction.
|
|
967
1242
|
*
|
|
968
1243
|
* @remarks
|
|
969
1244
|
* A thin immutable value object (mirroring {@link import('@orkestrel/tool').Tool}): the
|
|
970
1245
|
* constructor mints a fresh `id` (`crypto.randomUUID()`), copies the input's `name` /
|
|
971
1246
|
* `content`, resolves `priority` to the input's value or `0`, and carries the input's
|
|
972
|
-
* per-item `
|
|
1247
|
+
* per-item `override` ONLY when supplied (assigned when present, mirroring a
|
|
973
1248
|
* message's `images` / `calls` present-when-given convention — kept absent otherwise).
|
|
974
1249
|
* Never mutated after construction. An
|
|
975
1250
|
* {@link import('./InstructionManager.js').InstructionManager} keys it by `name` and
|
|
@@ -986,38 +1261,38 @@ var Instruction = class {
|
|
|
986
1261
|
name;
|
|
987
1262
|
content;
|
|
988
1263
|
priority;
|
|
989
|
-
|
|
1264
|
+
override;
|
|
990
1265
|
constructor(input) {
|
|
991
1266
|
this.id = crypto.randomUUID();
|
|
992
1267
|
this.name = input.name;
|
|
993
1268
|
this.content = input.content;
|
|
994
1269
|
this.priority = input.priority ?? 0;
|
|
995
|
-
if (input.
|
|
1270
|
+
if (input.override !== void 0) this.override = input.override;
|
|
996
1271
|
}
|
|
997
1272
|
};
|
|
998
1273
|
//#endregion
|
|
999
1274
|
//#region src/core/instructions/InstructionManager.ts
|
|
1000
1275
|
/**
|
|
1001
|
-
*
|
|
1002
|
-
*
|
|
1276
|
+
* Registers the immutable {@link Instruction}s a richer context assembles a directives
|
|
1277
|
+
* block from — keyed by `name`, listed by descending `priority`.
|
|
1003
1278
|
*
|
|
1004
1279
|
* @remarks
|
|
1005
1280
|
* - **Registry.** Instructions live in an insertion-ordered `Map` keyed by `name`;
|
|
1006
|
-
* `add` takes one {@link InstructionInput} or a batch
|
|
1281
|
+
* `add` takes one {@link InstructionInput} or a batch, MINTS each instruction's
|
|
1007
1282
|
* `id`, and a re-`add` of the same name OVERWRITES it (last write wins). `count` is the
|
|
1008
1283
|
* map size, `instruction(name)` looks one up, and `instructions()` lists them SORTED by
|
|
1009
1284
|
* descending `priority` (a stable sort, so equal priorities keep insertion order).
|
|
1010
|
-
* - **Build contract (with the manager-options override).** `
|
|
1011
|
-
* header a context renders the instructions under; `
|
|
1285
|
+
* - **Build contract (with the manager-options override).** `open` is the section
|
|
1286
|
+
* header a context renders the instructions under; `render(instruction)` renders one
|
|
1012
1287
|
* instruction (its `content`). Each ENCAPSULATES the cascade's `[options-override →
|
|
1013
1288
|
* built-in]` half: when `InstructionManagerOptions.format` supplies an `open` /
|
|
1014
|
-
* `render`, `
|
|
1289
|
+
* `render`, `open` / `render` return IT, else the built-in — so a richer context
|
|
1015
1290
|
* reads one consistent pair and layers the provider default + per-item override on top
|
|
1016
1291
|
* (see {@link import('../AgentContext.js').AgentContext}). The per-item
|
|
1017
|
-
* {@link InstructionInput.
|
|
1018
|
-
* - **Removal.** `remove` drops one by name, or a batch
|
|
1019
|
-
* removed; `clear` empties the registry.
|
|
1020
|
-
* - **Observable
|
|
1292
|
+
* {@link InstructionInput.override} is round-tripped onto the stored instruction.
|
|
1293
|
+
* - **Removal.** `remove` drops one by name, or a batch — `true` only when EVERY supplied
|
|
1294
|
+
* name was removed; `clear` empties the registry.
|
|
1295
|
+
* - **Observable.** The owned {@link emitter} ({@link InstructionManagerEventMap})
|
|
1021
1296
|
* carries `add` (the created instruction) / `remove` (the name) / `clear` for
|
|
1022
1297
|
* fire-and-forget observers. Every event is emitted directly, strictly AFTER the map
|
|
1023
1298
|
* mutation completes; the emitter isolates a listener throw and routes it to its `error`
|
|
@@ -1050,10 +1325,10 @@ var InstructionManager = class {
|
|
|
1050
1325
|
get count() {
|
|
1051
1326
|
return this.#instructions.size;
|
|
1052
1327
|
}
|
|
1053
|
-
get
|
|
1328
|
+
get open() {
|
|
1054
1329
|
return this.#format?.open ?? "## Instructions";
|
|
1055
1330
|
}
|
|
1056
|
-
get
|
|
1331
|
+
get format() {
|
|
1057
1332
|
return this.#format;
|
|
1058
1333
|
}
|
|
1059
1334
|
add(input) {
|
|
@@ -1066,13 +1341,13 @@ var InstructionManager = class {
|
|
|
1066
1341
|
instructions() {
|
|
1067
1342
|
return [...this.#instructions.values()].sort((a, b) => b.priority - a.priority);
|
|
1068
1343
|
}
|
|
1069
|
-
|
|
1344
|
+
render(instruction) {
|
|
1070
1345
|
return this.#format?.render?.(instruction) ?? instruction.content;
|
|
1071
1346
|
}
|
|
1072
1347
|
remove(names) {
|
|
1073
1348
|
if (isArray(names)) {
|
|
1074
|
-
let removed =
|
|
1075
|
-
for (const name of names) if (this.#delete(name)) removed =
|
|
1349
|
+
let removed = true;
|
|
1350
|
+
for (const name of names) if (!this.#delete(name)) removed = false;
|
|
1076
1351
|
return removed;
|
|
1077
1352
|
}
|
|
1078
1353
|
return this.#delete(names);
|
|
@@ -1096,8 +1371,8 @@ var InstructionManager = class {
|
|
|
1096
1371
|
//#endregion
|
|
1097
1372
|
//#region src/core/AgentContext.ts
|
|
1098
1373
|
/**
|
|
1099
|
-
*
|
|
1100
|
-
*
|
|
1374
|
+
* Assembles a provider request from the richer turn context — the optional system prompt, the
|
|
1375
|
+
* observable context managers (instructions / workspaces), the
|
|
1101
1376
|
* {@link ConversationManagerInterface} message source (whose active conversation IS `messages`),
|
|
1102
1377
|
* the {@link ToolManagerInterface} registry, and an active {@link ScopeInterface} changed through
|
|
1103
1378
|
* {@link AgentContextInterface.apply}.
|
|
@@ -1118,7 +1393,7 @@ var InstructionManager = class {
|
|
|
1118
1393
|
* {@link MessageManagerInterface} structurally — the same reference, no duplication), and
|
|
1119
1394
|
* `build()` folds that conversation's `view()` (its per-section summaries + live tail) as the
|
|
1120
1395
|
* AUTHORITATIVE message inclusion — the scope does NOT filter the conversation (it owns inclusion
|
|
1121
|
-
*
|
|
1396
|
+
* through compaction; scope filters only instructions / tools / workspace files). Because `messages`
|
|
1122
1397
|
* is read dynamically, an agent SWITCHES the active
|
|
1123
1398
|
* conversation BETWEEN runs (`conversations.switch(id)`) to serve MANY threads (the real
|
|
1124
1399
|
* multi-conversation pattern); switch between runs, not during a run, and use separate agents
|
|
@@ -1128,31 +1403,31 @@ var InstructionManager = class {
|
|
|
1128
1403
|
* (each as a block: the section's resolved `open` text, each item's resolved rendering, then
|
|
1129
1404
|
* any resolved `close` text) into ONE leading `system` message (prepended only when at least
|
|
1130
1405
|
* one part exists), then appends the ACTIVE conversation's `view()` (the conversation owns
|
|
1131
|
-
* message inclusion
|
|
1406
|
+
* message inclusion through compaction — the scope does NOT filter the conversation). Each
|
|
1132
1407
|
* `open` / item / `close`
|
|
1133
1408
|
* resolves INDEPENDENTLY, MOST-SPECIFIC-FIRST — `build()`'s optional `format` (a provider's
|
|
1134
1409
|
* per-section default) is the PROVIDER level: `open` = manager-options-override > provider >
|
|
1135
1410
|
* built-in; per item = item-override > manager-options-override > provider > built-in; `close` =
|
|
1136
1411
|
* manager-options-override > provider (NO built-in ⇒ no closing line when unset) (see
|
|
1137
1412
|
* {@link AgentContextInterface.build}). Passing NO `format` (and with no overrides / no per-item
|
|
1138
|
-
*
|
|
1139
|
-
* items, no closing line). The active workspace's scoped-in image files' `
|
|
1413
|
+
* override) reproduces the built-in framing byte-for-byte (each section is its built-in header +
|
|
1414
|
+
* items, no closing line). The active workspace's scoped-in image files' `base64` payload is attached to
|
|
1140
1415
|
* the LAST user message (a vision provider reads images off a user turn); when no user message
|
|
1141
1416
|
* exists the attachment is skipped. Built fresh each call (recomputed, never cached), so it
|
|
1142
1417
|
* always reflects the current managers / messages / scope / active workspace; it never mutates a
|
|
1143
1418
|
* manager or the stored messages.
|
|
1144
1419
|
* - **The ACTIVE workspace, rendered BY CARRIER — the SOLE document/image context.**
|
|
1145
|
-
* `workspaces.active` (when set) has its {@link FileInterface}s scope-filtered by `scope.files`,
|
|
1420
|
+
* `workspaces.active` (when set) has its {@link import('@orkestrel/workspace').FileInterface}s scope-filtered by `scope.files`,
|
|
1146
1421
|
* then split: TEXT files fold into a dedicated `## Workspace` system section (fenced reference
|
|
1147
|
-
* blocks — placed right after the instructions section), and IMAGE files' base64
|
|
1422
|
+
* blocks — placed right after the instructions section), and IMAGE files' `base64` payload attaches
|
|
1148
1423
|
* to the last user message. ACTIVE-ONLY — never the other registered workspaces; with no active
|
|
1149
1424
|
* workspace nothing renders for workspaces. `build()` OWNS this render (a `Workspace` /
|
|
1150
1425
|
* `WorkspaceManager` stays file-focused).
|
|
1151
1426
|
* - **Tools are structural, not in the prompt.** The registry is advertised to the provider
|
|
1152
|
-
*
|
|
1427
|
+
* through `tools.definitions()` (scope-filtered by the loop), NEVER serialized into the
|
|
1153
1428
|
* message array — so `build()`'s output carries no tool content, scoped or not.
|
|
1154
1429
|
* - **Event-free context; observable managers.** The context itself owns no Emitter; the
|
|
1155
|
-
* context managers each carry their own (the
|
|
1430
|
+
* context managers each carry their own (the push observation surface).
|
|
1156
1431
|
*
|
|
1157
1432
|
* @example
|
|
1158
1433
|
* ```ts
|
|
@@ -1207,12 +1482,12 @@ var AgentContext = class {
|
|
|
1207
1482
|
const parts = [];
|
|
1208
1483
|
if (this.#system !== void 0) parts.push(this.#system);
|
|
1209
1484
|
const instructions = filterAllowList(scope?.instructions, this.#instructions.instructions(), (one) => one.name);
|
|
1210
|
-
|
|
1485
|
+
const instructed = renderSection(resolveOpen(this.#instructions, format?.instructions), instructions, (one) => resolveItem(this.#instructions, format?.instructions, one), resolveClose(this.#instructions, format?.instructions));
|
|
1486
|
+
if (instructed !== void 0) parts.push(instructed);
|
|
1211
1487
|
const files = filterAllowList(scope?.files, this.#workspaces.active?.files() ?? [], (one) => one.path);
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1214
|
-
const
|
|
1215
|
-
const tail = this.#attach(conversation, this.#workspaceImages(files));
|
|
1488
|
+
const documented = renderSection(WORKSPACE_SECTION_HEADER, files.filter((file) => isText(file.content)), (file) => isText(file.content) ? renderFencedFile(file.path, file.content.language, file.content.text) : renderFencedFile(file.path, "text", ""), void 0);
|
|
1489
|
+
if (documented !== void 0) parts.push(documented);
|
|
1490
|
+
const tail = attachUserImages((this.#conversations.active ?? this.#ensure()).view(), collectImageData(files));
|
|
1216
1491
|
if (parts.length === 0) return tail;
|
|
1217
1492
|
return [{
|
|
1218
1493
|
id: crypto.randomUUID(),
|
|
@@ -1220,55 +1495,6 @@ var AgentContext = class {
|
|
|
1220
1495
|
content: parts.join("\n\n")
|
|
1221
1496
|
}, ...tail];
|
|
1222
1497
|
}
|
|
1223
|
-
#section(parts, open, items, format, close) {
|
|
1224
|
-
if (items.length === 0) return;
|
|
1225
|
-
const lines = [open, ...items.map(format)];
|
|
1226
|
-
if (close !== void 0) lines.push(close);
|
|
1227
|
-
parts.push(lines.join("\n\n"));
|
|
1228
|
-
}
|
|
1229
|
-
#header(manager, provider) {
|
|
1230
|
-
return manager.framing?.open ?? provider?.open ?? manager.description;
|
|
1231
|
-
}
|
|
1232
|
-
#footer(manager, provider) {
|
|
1233
|
-
return manager.framing?.close ?? provider?.close;
|
|
1234
|
-
}
|
|
1235
|
-
#render(manager, provider, item) {
|
|
1236
|
-
return item.format ?? manager.framing?.render?.(item) ?? provider?.render?.(item) ?? manager.format(item);
|
|
1237
|
-
}
|
|
1238
|
-
#attach(conversation, data) {
|
|
1239
|
-
if (data.length === 0) return conversation;
|
|
1240
|
-
let target = -1;
|
|
1241
|
-
for (let index = conversation.length - 1; index >= 0; index -= 1) if (conversation[index]?.role === "user") {
|
|
1242
|
-
target = index;
|
|
1243
|
-
break;
|
|
1244
|
-
}
|
|
1245
|
-
if (target === -1) return conversation;
|
|
1246
|
-
return conversation.map((message, index) => index === target ? this.#withImages(message, data) : message);
|
|
1247
|
-
}
|
|
1248
|
-
#withImages(message, data) {
|
|
1249
|
-
const images = [...message.images ?? [], ...data];
|
|
1250
|
-
return message.calls === void 0 ? {
|
|
1251
|
-
id: message.id,
|
|
1252
|
-
role: message.role,
|
|
1253
|
-
content: message.content,
|
|
1254
|
-
images
|
|
1255
|
-
} : {
|
|
1256
|
-
id: message.id,
|
|
1257
|
-
role: message.role,
|
|
1258
|
-
content: message.content,
|
|
1259
|
-
calls: message.calls,
|
|
1260
|
-
images
|
|
1261
|
-
};
|
|
1262
|
-
}
|
|
1263
|
-
#fenced(file) {
|
|
1264
|
-
if (isText(file.content)) return fencedFile(file.path, file.content.language, file.content.text);
|
|
1265
|
-
return fencedFile(file.path, "text", "");
|
|
1266
|
-
}
|
|
1267
|
-
#workspaceImages(files) {
|
|
1268
|
-
const data = [];
|
|
1269
|
-
for (const file of files) if (isBinary(file.content) && file.content.mime.startsWith("image/")) data.push(file.content.data);
|
|
1270
|
-
return data;
|
|
1271
|
-
}
|
|
1272
1498
|
#ensure() {
|
|
1273
1499
|
const conversation = this.#conversations.add();
|
|
1274
1500
|
return this.#conversations.active ?? conversation;
|
|
@@ -1277,9 +1503,9 @@ var AgentContext = class {
|
|
|
1277
1503
|
//#endregion
|
|
1278
1504
|
//#region src/core/Channel.ts
|
|
1279
1505
|
/**
|
|
1280
|
-
*
|
|
1281
|
-
* and ends it (`close` / `fail`) regardless of consumption; a consumer READS them back
|
|
1282
|
-
* live
|
|
1506
|
+
* Buffers chunks in a minimal unbounded async channel — the eager pump WRITES them in
|
|
1507
|
+
* (`push`) and ends it (`close` / `fail`) regardless of consumption; a consumer READS them back
|
|
1508
|
+
* live through the `drain` async-iterator. Decoupling write from read is what lets a
|
|
1283
1509
|
* producer make progress without a consumer pulling.
|
|
1284
1510
|
*
|
|
1285
1511
|
* @remarks
|
|
@@ -1340,15 +1566,15 @@ var Channel = class {
|
|
|
1340
1566
|
//#endregion
|
|
1341
1567
|
//#region src/core/Agent.ts
|
|
1342
1568
|
/**
|
|
1343
|
-
*
|
|
1344
|
-
*
|
|
1569
|
+
* Composes a {@link ProviderInterface}, an {@link AgentContext}, and a
|
|
1570
|
+
* {@link ToolManagerInterface} into a bounded context → provider → tools → repeat
|
|
1345
1571
|
* turn, exposed as both a one-shot `generate` and a live `stream`.
|
|
1346
1572
|
*
|
|
1347
1573
|
* @remarks
|
|
1348
1574
|
* - **One loop, two faces.** A single private async generator (`#run`) drives the
|
|
1349
1575
|
* whole turn. `stream` kicks off an eager pump that iterates `#run` into a private
|
|
1350
1576
|
* {@link Channel}, settling `result` from the run's outcome — so `result` settles
|
|
1351
|
-
* whether or not the live `events` are drained; `generate`
|
|
1577
|
+
* whether or not the live `events` are drained; `generate` awaits that same
|
|
1352
1578
|
* settled `result` — so the two can never diverge.
|
|
1353
1579
|
* - **The turn.** `#run` builds the provider input once (`context.build()` into a
|
|
1354
1580
|
* working array) then loops up to `limit`: drive `provider.stream(...)` accumulating
|
|
@@ -1357,7 +1583,7 @@ var Channel = class {
|
|
|
1357
1583
|
* tools, append the assistant turn, `execute` them, yield a `tool` chunk per call,
|
|
1358
1584
|
* append each tool result message, and continue; otherwise append the final
|
|
1359
1585
|
* assistant message and stop.
|
|
1360
|
-
* - **Bounded.** Each run arms one cancel
|
|
1586
|
+
* - **Bounded.** Each run arms one cancel through `createAbort({ signal: AbortSignal.any([
|
|
1361
1587
|
* …]) })` folding the external `signal`, the `timeout` deadline, and the `budget`
|
|
1362
1588
|
* signal; `abort()` fires it. Any trip stops the loop and commits a PARTIAL result
|
|
1363
1589
|
* (the `result` promise RESOLVES, never rejects, on a cancel) — only a genuine
|
|
@@ -1380,6 +1606,7 @@ var Channel = class {
|
|
|
1380
1606
|
* ```
|
|
1381
1607
|
*/
|
|
1382
1608
|
var Agent = class {
|
|
1609
|
+
#id;
|
|
1383
1610
|
#provider;
|
|
1384
1611
|
#context;
|
|
1385
1612
|
#limit;
|
|
@@ -1391,10 +1618,10 @@ var Agent = class {
|
|
|
1391
1618
|
#window;
|
|
1392
1619
|
#strict;
|
|
1393
1620
|
#emitter;
|
|
1394
|
-
|
|
1395
|
-
#status = "idle";
|
|
1621
|
+
#settled = "idle";
|
|
1396
1622
|
#runs = /* @__PURE__ */ new Set();
|
|
1397
1623
|
constructor(provider, options) {
|
|
1624
|
+
this.#id = crypto.randomUUID();
|
|
1398
1625
|
this.#provider = provider;
|
|
1399
1626
|
this.#context = new AgentContext({
|
|
1400
1627
|
...options?.system === void 0 ? {} : { system: options.system },
|
|
@@ -1417,11 +1644,14 @@ var Agent = class {
|
|
|
1417
1644
|
...options?.error === void 0 ? {} : { error: options.error }
|
|
1418
1645
|
});
|
|
1419
1646
|
}
|
|
1647
|
+
get id() {
|
|
1648
|
+
return this.#id;
|
|
1649
|
+
}
|
|
1420
1650
|
get emitter() {
|
|
1421
1651
|
return this.#emitter;
|
|
1422
1652
|
}
|
|
1423
1653
|
get status() {
|
|
1424
|
-
return this.#
|
|
1654
|
+
return this.#runs.size > 0 ? "running" : this.#settled;
|
|
1425
1655
|
}
|
|
1426
1656
|
get context() {
|
|
1427
1657
|
return this.#context;
|
|
@@ -1440,18 +1670,10 @@ var Agent = class {
|
|
|
1440
1670
|
const signal = this.#parents(timeout, budget, options?.signal);
|
|
1441
1671
|
const abort = createAbort(signal === void 0 ? {} : { signal });
|
|
1442
1672
|
this.#runs.add(abort);
|
|
1443
|
-
this.#status = "running";
|
|
1444
1673
|
this.#emitter.emit("start", this.id);
|
|
1445
|
-
const state = { outcome: {
|
|
1446
|
-
content: "",
|
|
1447
|
-
thinking: void 0,
|
|
1448
|
-
usage: void 0,
|
|
1449
|
-
partial: false,
|
|
1450
|
-
exhausted: false
|
|
1451
|
-
} };
|
|
1452
1674
|
const channel = new Channel();
|
|
1453
|
-
const settled =
|
|
1454
|
-
this.#pump(abort,
|
|
1675
|
+
const settled = Promise.withResolvers();
|
|
1676
|
+
this.#pump(abort, timeout, channel, settled, options?.think, options?.schema, limit, budget);
|
|
1455
1677
|
settled.promise.catch(() => {});
|
|
1456
1678
|
return {
|
|
1457
1679
|
events: this.#events(channel, abort),
|
|
@@ -1462,26 +1684,38 @@ var Agent = class {
|
|
|
1462
1684
|
abort(reason) {
|
|
1463
1685
|
for (const abort of [...this.#runs]) abort.abort(reason);
|
|
1464
1686
|
}
|
|
1465
|
-
async #pump(abort,
|
|
1687
|
+
async #pump(abort, timeout, channel, settled, think, schema, limit, budget) {
|
|
1466
1688
|
let failure;
|
|
1689
|
+
let outcome = {
|
|
1690
|
+
content: "",
|
|
1691
|
+
thinking: void 0,
|
|
1692
|
+
usage: void 0,
|
|
1693
|
+
partial: false,
|
|
1694
|
+
exhausted: false
|
|
1695
|
+
};
|
|
1467
1696
|
try {
|
|
1468
|
-
|
|
1697
|
+
const run = this.#run(abort, think, schema, limit, budget);
|
|
1698
|
+
let next = await run.next();
|
|
1699
|
+
while (next.done !== true) {
|
|
1700
|
+
channel.push(next.value);
|
|
1701
|
+
next = await run.next();
|
|
1702
|
+
}
|
|
1703
|
+
outcome = next.value;
|
|
1469
1704
|
} catch (error) {
|
|
1470
1705
|
failure = { error };
|
|
1471
1706
|
} finally {
|
|
1472
1707
|
timeout?.clear();
|
|
1473
1708
|
this.#runs.delete(abort);
|
|
1474
1709
|
if (failure === void 0) {
|
|
1475
|
-
|
|
1476
|
-
this.#status = "done";
|
|
1710
|
+
this.#settled = "done";
|
|
1477
1711
|
channel.close();
|
|
1478
|
-
const result =
|
|
1712
|
+
const result = assembleResult(outcome);
|
|
1479
1713
|
settled.resolve(result);
|
|
1480
1714
|
if (outcome.exhausted) this.#emitter.emit("exhaust", limit);
|
|
1481
1715
|
else if (outcome.partial) this.#emitter.emit("abort", abort.signal.reason);
|
|
1482
1716
|
this.#emitter.emit("finish", result);
|
|
1483
1717
|
} else {
|
|
1484
|
-
this.#
|
|
1718
|
+
this.#settled = "error";
|
|
1485
1719
|
channel.fail(failure.error);
|
|
1486
1720
|
settled.reject(failure.error);
|
|
1487
1721
|
this.#emitter.emit("error", failure.error);
|
|
@@ -1495,7 +1729,7 @@ var Agent = class {
|
|
|
1495
1729
|
abort.abort();
|
|
1496
1730
|
}
|
|
1497
1731
|
}
|
|
1498
|
-
async *#run(abort,
|
|
1732
|
+
async *#run(abort, think, schema, limit, budget) {
|
|
1499
1733
|
const messages = [...this.#context.build(this.#provider.format)];
|
|
1500
1734
|
const tools = this.#context.tools;
|
|
1501
1735
|
let content = "";
|
|
@@ -1503,11 +1737,13 @@ var Agent = class {
|
|
|
1503
1737
|
let usage;
|
|
1504
1738
|
let pending = false;
|
|
1505
1739
|
let broke = false;
|
|
1506
|
-
|
|
1740
|
+
let partial = false;
|
|
1741
|
+
let exhausted = false;
|
|
1742
|
+
let futile = false;
|
|
1507
1743
|
const compacting = this.#window !== void 0 && this.#context.conversations.active?.summarizable === true;
|
|
1508
1744
|
if (compacting) {
|
|
1509
1745
|
this.#window?.clear();
|
|
1510
|
-
if (!abort.signal.aborted) await this.#trim(messages,
|
|
1746
|
+
if (!abort.signal.aborted) futile = await this.#trim(messages, false);
|
|
1511
1747
|
}
|
|
1512
1748
|
for (let turn = 0; turn < limit; turn += 1) {
|
|
1513
1749
|
this.#emitter.emit("turn", turn);
|
|
@@ -1515,20 +1751,14 @@ var Agent = class {
|
|
|
1515
1751
|
await this.#scheduler?.yield({ signal: abort.signal });
|
|
1516
1752
|
} catch (error) {
|
|
1517
1753
|
if (abort.signal.aborted) {
|
|
1518
|
-
|
|
1519
|
-
...state.outcome,
|
|
1520
|
-
partial: true
|
|
1521
|
-
};
|
|
1754
|
+
partial = true;
|
|
1522
1755
|
broke = true;
|
|
1523
1756
|
break;
|
|
1524
1757
|
}
|
|
1525
1758
|
throw error;
|
|
1526
1759
|
}
|
|
1527
1760
|
if (abort.signal.aborted) {
|
|
1528
|
-
|
|
1529
|
-
...state.outcome,
|
|
1530
|
-
partial: true
|
|
1531
|
-
};
|
|
1761
|
+
partial = true;
|
|
1532
1762
|
broke = true;
|
|
1533
1763
|
break;
|
|
1534
1764
|
}
|
|
@@ -1554,7 +1784,7 @@ var Agent = class {
|
|
|
1554
1784
|
} catch (error) {
|
|
1555
1785
|
if (abort.signal.aborted) {
|
|
1556
1786
|
if (isProviderAbortError(error)) {
|
|
1557
|
-
if (error.partial.thinking !== void 0) thinking =
|
|
1787
|
+
if (error.partial.thinking !== void 0) thinking = joinThinking(thinking, error.partial.thinking);
|
|
1558
1788
|
if (error.partial.usage !== void 0) {
|
|
1559
1789
|
const abortUsage = sanitizeUsage(error.partial.usage);
|
|
1560
1790
|
budget?.consume({
|
|
@@ -1562,19 +1792,16 @@ var Agent = class {
|
|
|
1562
1792
|
completion: Math.max(0, abortUsage.completion - charged),
|
|
1563
1793
|
total: Math.max(0, abortUsage.total - charged)
|
|
1564
1794
|
});
|
|
1565
|
-
usage =
|
|
1795
|
+
usage = sumUsage(usage, abortUsage);
|
|
1566
1796
|
}
|
|
1567
1797
|
}
|
|
1568
|
-
|
|
1569
|
-
...state.outcome,
|
|
1570
|
-
partial: true
|
|
1571
|
-
};
|
|
1798
|
+
partial = true;
|
|
1572
1799
|
broke = true;
|
|
1573
1800
|
break;
|
|
1574
1801
|
}
|
|
1575
1802
|
throw error;
|
|
1576
1803
|
}
|
|
1577
|
-
if (result.thinking !== void 0 && result.thinking.length > 0) thinking =
|
|
1804
|
+
if (result.thinking !== void 0 && result.thinking.length > 0) thinking = joinThinking(thinking, result.thinking);
|
|
1578
1805
|
if (result.usage !== void 0) {
|
|
1579
1806
|
const resultUsage = sanitizeUsage(result.usage);
|
|
1580
1807
|
budget?.consume({
|
|
@@ -1582,10 +1809,10 @@ var Agent = class {
|
|
|
1582
1809
|
completion: Math.max(0, resultUsage.completion - charged),
|
|
1583
1810
|
total: Math.max(0, resultUsage.total - charged)
|
|
1584
1811
|
});
|
|
1585
|
-
usage =
|
|
1812
|
+
usage = sumUsage(usage, resultUsage);
|
|
1586
1813
|
this.#emitter.emit("usage", resultUsage);
|
|
1587
1814
|
yield {
|
|
1588
|
-
|
|
1815
|
+
category: "usage",
|
|
1589
1816
|
usage: resultUsage
|
|
1590
1817
|
};
|
|
1591
1818
|
}
|
|
@@ -1603,7 +1830,7 @@ var Agent = class {
|
|
|
1603
1830
|
if (call === void 0 || outcomeResult === void 0) continue;
|
|
1604
1831
|
this.#emitter.emit("tool", call, outcomeResult);
|
|
1605
1832
|
yield {
|
|
1606
|
-
|
|
1833
|
+
category: "tool",
|
|
1607
1834
|
call,
|
|
1608
1835
|
result: outcomeResult
|
|
1609
1836
|
};
|
|
@@ -1613,7 +1840,7 @@ var Agent = class {
|
|
|
1613
1840
|
});
|
|
1614
1841
|
messages.push(toolMessage);
|
|
1615
1842
|
}
|
|
1616
|
-
if (compacting) await this.#trim(messages,
|
|
1843
|
+
if (compacting && !futile) futile = await this.#trim(messages, true);
|
|
1617
1844
|
pending = true;
|
|
1618
1845
|
continue;
|
|
1619
1846
|
}
|
|
@@ -1626,37 +1853,35 @@ var Agent = class {
|
|
|
1626
1853
|
broke = true;
|
|
1627
1854
|
break;
|
|
1628
1855
|
}
|
|
1629
|
-
if (!broke && pending)
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
state.outcome = {
|
|
1635
|
-
...state.outcome,
|
|
1856
|
+
if (!broke && pending) {
|
|
1857
|
+
partial = true;
|
|
1858
|
+
exhausted = !abort.signal.aborted;
|
|
1859
|
+
}
|
|
1860
|
+
return {
|
|
1636
1861
|
content,
|
|
1637
1862
|
thinking,
|
|
1638
|
-
usage
|
|
1863
|
+
usage,
|
|
1864
|
+
partial,
|
|
1865
|
+
exhausted
|
|
1639
1866
|
};
|
|
1640
1867
|
}
|
|
1641
|
-
async #trim(messages,
|
|
1868
|
+
async #trim(messages, latch) {
|
|
1642
1869
|
const conversation = this.#context.conversations.active;
|
|
1643
|
-
if (this.#window === void 0 || conversation?.summarizable !== true
|
|
1870
|
+
if (this.#window === void 0 || conversation?.summarizable !== true) return false;
|
|
1644
1871
|
this.#window.clear();
|
|
1645
1872
|
this.#window.consume(messages);
|
|
1646
|
-
if (!this.#window.exhausted) return;
|
|
1873
|
+
if (!this.#window.exhausted) return false;
|
|
1647
1874
|
let section;
|
|
1648
1875
|
try {
|
|
1649
1876
|
section = await conversation.compact();
|
|
1650
1877
|
} catch (error) {
|
|
1651
|
-
this.#emitter.emit("
|
|
1878
|
+
this.#emitter.emit("fault", error);
|
|
1652
1879
|
if (this.#strict) throw error;
|
|
1653
|
-
return;
|
|
1654
|
-
}
|
|
1655
|
-
if (section === void 0) {
|
|
1656
|
-
if (latchFutile) compaction.state = { futile: true };
|
|
1657
|
-
return;
|
|
1880
|
+
return false;
|
|
1658
1881
|
}
|
|
1882
|
+
if (section === void 0) return latch;
|
|
1659
1883
|
messages.splice(0, messages.length, ...this.#context.build(this.#provider.format));
|
|
1884
|
+
return false;
|
|
1660
1885
|
}
|
|
1661
1886
|
async #authorize(tools, calls) {
|
|
1662
1887
|
if (this.#authority === void 0) return tools.execute(calls);
|
|
@@ -1668,32 +1893,21 @@ var Agent = class {
|
|
|
1668
1893
|
try {
|
|
1669
1894
|
decision = authority.evaluate({ call });
|
|
1670
1895
|
} catch (error) {
|
|
1671
|
-
const reason =
|
|
1672
|
-
denials.set(call.id,
|
|
1896
|
+
const reason = errorToMessage(error);
|
|
1897
|
+
denials.set(call.id, denyCall(call, reason));
|
|
1673
1898
|
this.#emitter.emit("deny", call, reason);
|
|
1674
1899
|
continue;
|
|
1675
1900
|
}
|
|
1676
1901
|
if (decision.allowed) allowed.push(call);
|
|
1677
1902
|
else {
|
|
1678
|
-
denials.set(call.id,
|
|
1903
|
+
denials.set(call.id, denyCall(call, decision.reason));
|
|
1679
1904
|
this.#emitter.emit("deny", call, decision.reason);
|
|
1680
1905
|
}
|
|
1681
1906
|
}
|
|
1682
1907
|
const executed = allowed.length > 0 ? await tools.execute(allowed) : [];
|
|
1683
1908
|
const byId = new Map(denials);
|
|
1684
1909
|
for (const result of executed) byId.set(result.id, result);
|
|
1685
|
-
return calls.map((call) => byId.get(call.id) ??
|
|
1686
|
-
}
|
|
1687
|
-
#denial(call, reason) {
|
|
1688
|
-
return {
|
|
1689
|
-
success: false,
|
|
1690
|
-
id: call.id,
|
|
1691
|
-
name: call.name,
|
|
1692
|
-
error: reason !== void 0 ? `denied: ${reason}` : "denied by authority"
|
|
1693
|
-
};
|
|
1694
|
-
}
|
|
1695
|
-
#reason(error) {
|
|
1696
|
-
return error instanceof Error ? error.message : String(error);
|
|
1910
|
+
return calls.map((call) => byId.get(call.id) ?? denyCall(call, void 0));
|
|
1697
1911
|
}
|
|
1698
1912
|
async *#provide(messages, signal, definitions, think, schema, onDelta) {
|
|
1699
1913
|
const options = {};
|
|
@@ -1703,14 +1917,14 @@ var Agent = class {
|
|
|
1703
1917
|
let next = await generator.next();
|
|
1704
1918
|
while (!next.done) {
|
|
1705
1919
|
const delta = next.value;
|
|
1706
|
-
if (delta.
|
|
1920
|
+
if (delta.channel === "content") {
|
|
1707
1921
|
onDelta(delta.text);
|
|
1708
1922
|
yield {
|
|
1709
|
-
|
|
1923
|
+
category: "token",
|
|
1710
1924
|
content: delta.text
|
|
1711
1925
|
};
|
|
1712
1926
|
} else yield {
|
|
1713
|
-
|
|
1927
|
+
category: "think",
|
|
1714
1928
|
content: delta.text
|
|
1715
1929
|
};
|
|
1716
1930
|
next = await generator.next();
|
|
@@ -1727,34 +1941,13 @@ var Agent = class {
|
|
|
1727
1941
|
if (signals.length === 1) return signals[0];
|
|
1728
1942
|
return AbortSignal.any(signals);
|
|
1729
1943
|
}
|
|
1730
|
-
#thought(running, next) {
|
|
1731
|
-
return running === void 0 ? next : `${running}\n\n${next}`;
|
|
1732
|
-
}
|
|
1733
|
-
#sum(running, next) {
|
|
1734
|
-
if (running === void 0) return next;
|
|
1735
|
-
return {
|
|
1736
|
-
prompt: running.prompt + next.prompt,
|
|
1737
|
-
completion: running.completion + next.completion,
|
|
1738
|
-
total: running.total + next.total
|
|
1739
|
-
};
|
|
1740
|
-
}
|
|
1741
|
-
#result(outcome) {
|
|
1742
|
-
const result = {
|
|
1743
|
-
content: outcome.content,
|
|
1744
|
-
partial: outcome.partial
|
|
1745
|
-
};
|
|
1746
|
-
if (outcome.thinking !== void 0) result.thinking = outcome.thinking;
|
|
1747
|
-
if (outcome.usage !== void 0) result.usage = outcome.usage;
|
|
1748
|
-
return result;
|
|
1749
|
-
}
|
|
1750
1944
|
};
|
|
1751
1945
|
//#endregion
|
|
1752
1946
|
//#region src/core/AgentRegistry.ts
|
|
1753
1947
|
/**
|
|
1754
|
-
*
|
|
1755
|
-
*
|
|
1756
|
-
*
|
|
1757
|
-
* job's names + data.
|
|
1948
|
+
* Makes a durable, JSON-serializable {@link AgentJobInput} runnable — holds the named
|
|
1949
|
+
* pools of live, non-serializable pieces (providers, tools, authorities, schedulers) and
|
|
1950
|
+
* rehydrates a seeded, signal-wired {@link Agent} from a job's names + data.
|
|
1758
1951
|
*
|
|
1759
1952
|
* @remarks
|
|
1760
1953
|
* - **Why it exists.** An `AgentJobInput` is serializable so it can survive a crash in a
|
|
@@ -1762,18 +1955,18 @@ var Agent = class {
|
|
|
1762
1955
|
* schedulers carrying functions) cannot serialize. The registry closes that gap:
|
|
1763
1956
|
* construct it once with the live pools, then a queue / runner handler calls `build`
|
|
1764
1957
|
* on each (possibly restored) job to get a ready agent.
|
|
1765
|
-
* - **Accessors throw on a miss
|
|
1766
|
-
*
|
|
1767
|
-
* when it is absent — an unknown name in a
|
|
1768
|
-
* error that must fail LOUDLY at build time, never
|
|
1769
|
-
* run an agent missing a dependency.
|
|
1958
|
+
* - **Accessors throw on a miss.** `provider` / `tool` / `authority` / `scheduler` resolve a
|
|
1959
|
+
* name against their pool and THROW an {@link AgentError} carrying `code: 'REGISTRY'` and
|
|
1960
|
+
* the message `unknown <category>: <name>` when it is absent — an unknown name in a
|
|
1961
|
+
* rehydrated job is a programmer / config error that must fail LOUDLY at build time, never
|
|
1962
|
+
* silently resolve to `undefined` and run an agent missing a dependency.
|
|
1770
1963
|
* - **`build` rehydrates.** Resolve the job's `provider`; assemble a fresh
|
|
1771
1964
|
* {@link ToolManager} from the `tools` names; rebuild the token `budget` from its
|
|
1772
1965
|
* ceiling (`createTokenBudget({ max })`); resolve the optional `authority` /
|
|
1773
1966
|
* `scheduler` names; construct the {@link Agent} with `system` / `limit` / `timeout` /
|
|
1774
1967
|
* the threaded `signal`; seed its context with the job's `messages`; return it. The
|
|
1775
1968
|
* `signal` is the queue attempt's / runner unit's cancel, so a bounded abort propagates
|
|
1776
|
-
* into the agent (which commits a partial — the job's `
|
|
1969
|
+
* into the agent (which commits a partial — the job's `partial` policy then
|
|
1777
1970
|
* decides success vs. retry).
|
|
1778
1971
|
* - **Event-free.** A pure resolver — no Emitter, no events.
|
|
1779
1972
|
*
|
|
@@ -1839,15 +2032,15 @@ var AgentRegistry = class {
|
|
|
1839
2032
|
}
|
|
1840
2033
|
#resolve(pool, category, name) {
|
|
1841
2034
|
const value = pool.get(name);
|
|
1842
|
-
if (value === void 0) throw new
|
|
2035
|
+
if (value === void 0) throw new AgentError("REGISTRY", `unknown ${category}: ${name}`);
|
|
1843
2036
|
return value;
|
|
1844
2037
|
}
|
|
1845
2038
|
};
|
|
1846
2039
|
//#endregion
|
|
1847
2040
|
//#region src/core/Authority.ts
|
|
1848
2041
|
/**
|
|
1849
|
-
*
|
|
1850
|
-
*
|
|
2042
|
+
* Gates the agent loop's tool calls — the synchronous policy consulted before each call
|
|
2043
|
+
* runs, turning one {@link AuthorityContext} into an {@link AuthorityDecision}.
|
|
1851
2044
|
*
|
|
1852
2045
|
* @remarks
|
|
1853
2046
|
* - **Ordered, first-match-wins.** `evaluate` walks the configured rules in order and
|
|
@@ -1864,8 +2057,7 @@ var AgentRegistry = class {
|
|
|
1864
2057
|
* back to the model as a denial {@link import('@orkestrel/tool').ToolResult} (a `tool`
|
|
1865
2058
|
* chunk + a tool message) instead of being executed — no tool run, no budget cost —
|
|
1866
2059
|
* so the model sees the denial and can react.
|
|
1867
|
-
* - **Synchronous
|
|
1868
|
-
* deferred to a later chunk; `evaluate` returns a verdict directly.
|
|
2060
|
+
* - **Synchronous.** `evaluate` returns the verdict directly.
|
|
1869
2061
|
* - **Event-free.** A purely functional gate — no Emitter, no events.
|
|
1870
2062
|
*
|
|
1871
2063
|
* @example
|
|
@@ -1898,9 +2090,111 @@ var Authority = class {
|
|
|
1898
2090
|
}
|
|
1899
2091
|
};
|
|
1900
2092
|
//#endregion
|
|
2093
|
+
//#region src/core/validators.ts
|
|
2094
|
+
/**
|
|
2095
|
+
* Checks whether an `unknown` is structurally a {@link Message} record — the per-message step of
|
|
2096
|
+
* the {@link isConversationSnapshot} read-boundary narrow (a total guard over an untrusted storage
|
|
2097
|
+
* read, never an assertion). The conversation analogue of
|
|
2098
|
+
* {@link import('@orkestrel/workspace').isFile}.
|
|
2099
|
+
*
|
|
2100
|
+
* @remarks
|
|
2101
|
+
* A total guard (it NEVER throws — adversarial input returns `false`). It checks the message's
|
|
2102
|
+
* SHAPE: a record with a `string` `id`, a `string` `role`, a `string` `content`, and — WHEN present
|
|
2103
|
+
* — a `calls` array EVERY element of which is a valid
|
|
2104
|
+
* {@link import('@orkestrel/tool').ToolCall} ({@link isToolCall} — the
|
|
2105
|
+
* fail-closed deepening: a tampered `calls` element rejects the message, so the snapshot
|
|
2106
|
+
* reads back as absent rather than replaying a malformed call) and an `images` that is an array
|
|
2107
|
+
* (an absent optional passes). The `role` is left as a broad `string` here (an open
|
|
2108
|
+
* {@link import('./types.js').MessageRole}, so any storage-read role string is accepted
|
|
2109
|
+
* defensively rather than rejected against the current literal set). Enough to safely impose the
|
|
2110
|
+
* {@link Message} type at
|
|
2111
|
+
* a storage boundary WITHOUT a cast.
|
|
2112
|
+
*
|
|
2113
|
+
* @param value - The value to test (one element of a snapshot's `messages` / a section's `messages`)
|
|
2114
|
+
* @returns True if `value` has the structural shape of a {@link Message}; false otherwise
|
|
2115
|
+
*
|
|
2116
|
+
* @example
|
|
2117
|
+
* ```ts
|
|
2118
|
+
* isMessage({ id: '1', role: 'user', content: 'hi' }) // true
|
|
2119
|
+
* isMessage({ id: '1', role: 'assistant', content: '', calls: [] }) // true
|
|
2120
|
+
* isMessage({ id: '1', role: 'user' }) // false (missing content)
|
|
2121
|
+
* isMessage({ id: '1', role: 'assistant', content: '', calls: [null] }) // false (malformed call)
|
|
2122
|
+
* ```
|
|
2123
|
+
*/
|
|
2124
|
+
function isMessage(value) {
|
|
2125
|
+
if (!isRecord(value)) return false;
|
|
2126
|
+
if (!isString(value.id) || !isString(value.role) || !isString(value.content)) return false;
|
|
2127
|
+
if (value.calls !== void 0 && !(isArray(value.calls) && value.calls.every(isToolCall))) return false;
|
|
2128
|
+
return value.images === void 0 || isArray(value.images);
|
|
2129
|
+
}
|
|
2130
|
+
/**
|
|
2131
|
+
* Checks whether an `unknown` is structurally a {@link Section} record — the per-section step of
|
|
2132
|
+
* the {@link isConversationSnapshot} read-boundary narrow (a total guard over an untrusted storage
|
|
2133
|
+
* read, never an assertion).
|
|
2134
|
+
*
|
|
2135
|
+
* @remarks
|
|
2136
|
+
* A total guard (it NEVER throws — adversarial input returns `false`). It checks the section's
|
|
2137
|
+
* SHAPE: a record with a `string` `id`, a `string` `summary`, and a `messages` array EVERY element
|
|
2138
|
+
* of which is a valid {@link Message} record ({@link isMessage}). Enough to safely impose
|
|
2139
|
+
* the {@link Section} type at a storage boundary WITHOUT a cast.
|
|
2140
|
+
*
|
|
2141
|
+
* @param value - The value to test (one element of a snapshot's `sections` array)
|
|
2142
|
+
* @returns True if `value` has the structural shape of a {@link Section}; false otherwise
|
|
2143
|
+
*
|
|
2144
|
+
* @example
|
|
2145
|
+
* ```ts
|
|
2146
|
+
* isSection({ id: 's', summary: 'recap', messages: [{ id: '1', role: 'user', content: 'hi' }] }) // true
|
|
2147
|
+
* isSection({ id: 's', summary: 'recap', messages: 'nope' }) // false
|
|
2148
|
+
* isSection({ id: 's', messages: [] }) // false (missing summary)
|
|
2149
|
+
* ```
|
|
2150
|
+
*/
|
|
2151
|
+
function isSection(value) {
|
|
2152
|
+
if (!isRecord(value)) return false;
|
|
2153
|
+
if (!isString(value.id) || !isString(value.summary)) return false;
|
|
2154
|
+
return isArray(value.messages) && value.messages.every(isMessage);
|
|
2155
|
+
}
|
|
2156
|
+
/**
|
|
2157
|
+
* Narrows an `unknown` to a {@link ConversationSnapshot} — the total boundary guard for an
|
|
2158
|
+
* UNTRUSTED snapshot read (a storage row a
|
|
2159
|
+
* {@link import('./conversations/stores/DatabaseConversationStore.js').DatabaseConversationStore}
|
|
2160
|
+
* reads back from its opaque JSON column, a snapshot loaded from disk). The EXACT analogue of
|
|
2161
|
+
* {@link import('@orkestrel/workspace').isWorkspaceSnapshot}.
|
|
2162
|
+
*
|
|
2163
|
+
* @remarks
|
|
2164
|
+
* A total guard (it NEVER throws — adversarial input returns `false`). It checks the snapshot's
|
|
2165
|
+
* SHAPE: a `string` `id`, an OPTIONAL `string` `summary` (present-or-absent — the rollup is
|
|
2166
|
+
* `undefined` until the first compaction), a `sections` array EVERY element of which is a valid
|
|
2167
|
+
* {@link Section} ({@link isSection}), and a `messages` array EVERY element of which is a
|
|
2168
|
+
* valid {@link Message} ({@link isMessage}) — enough to safely impose the
|
|
2169
|
+
* {@link ConversationSnapshot} type at a storage boundary WITHOUT a cast. The structural twin of
|
|
2170
|
+
* {@link import('@orkestrel/workspace').isWorkspaceSnapshot}. A malformed blob (a non-record, a missing / non-string `id`, a
|
|
2171
|
+
* non-string `summary` when present, a non-array `sections` / `messages`, or any malformed
|
|
2172
|
+
* element) resolves `false`, so a
|
|
2173
|
+
* {@link import('./conversations/stores/DatabaseConversationStore.js').DatabaseConversationStore}
|
|
2174
|
+
* read yields `undefined` rather than a broken conversation.
|
|
2175
|
+
*
|
|
2176
|
+
* @param value - The value to test (an opaque storage read)
|
|
2177
|
+
* @returns True if `value` has the structural shape of a {@link ConversationSnapshot}; false otherwise
|
|
2178
|
+
*
|
|
2179
|
+
* @example
|
|
2180
|
+
* ```ts
|
|
2181
|
+
* isConversationSnapshot({ id: 'c1', sections: [], messages: [] }) // true
|
|
2182
|
+
* isConversationSnapshot({ id: 'c1', summary: 'recap', sections: [], messages: [] }) // true
|
|
2183
|
+
* isConversationSnapshot({ id: 'c1', sections: 'nope', messages: [] }) // false
|
|
2184
|
+
* isConversationSnapshot({ sections: [], messages: [] }) // false (missing id)
|
|
2185
|
+
* ```
|
|
2186
|
+
*/
|
|
2187
|
+
function isConversationSnapshot(value) {
|
|
2188
|
+
if (!isRecord(value)) return false;
|
|
2189
|
+
if (!isString(value.id)) return false;
|
|
2190
|
+
if (value.summary !== void 0 && !isString(value.summary)) return false;
|
|
2191
|
+
if (!isArray(value.sections) || !value.sections.every(isSection)) return false;
|
|
2192
|
+
return isArray(value.messages) && value.messages.every(isMessage);
|
|
2193
|
+
}
|
|
2194
|
+
//#endregion
|
|
1901
2195
|
//#region src/core/conversations/stores/DatabaseConversationStore.ts
|
|
1902
2196
|
/**
|
|
1903
|
-
*
|
|
2197
|
+
* Backs a {@link ConversationStoreInterface} with one table of the `databases` layer — a
|
|
1904
2198
|
* conversation's durable state IS a row, so persistence reduces to keyed point-access (`get` / `set`
|
|
1905
2199
|
* / `delete`) over a {@link TableInterface}, the driver-pluggable twin of the plain-`Map`
|
|
1906
2200
|
* {@link import('./MemoryConversationStore.js').MemoryConversationStore}. The EXACT twin of
|
|
@@ -1926,19 +2220,20 @@ var Authority = class {
|
|
|
1926
2220
|
* - **`set(snapshot)` upserts under the snapshot's OWN `id`** (no separate id param) — it writes
|
|
1927
2221
|
* the row `{ id: snapshot.id, snapshot }`.
|
|
1928
2222
|
* - **`get(id)` resolves the stored snapshot for an id**, narrowing the opaque JSON column back to
|
|
1929
|
-
* a {@link ConversationSnapshot} ({@link import('../../
|
|
1930
|
-
*
|
|
2223
|
+
* a {@link ConversationSnapshot} ({@link import('../../validators.js').isConversationSnapshot} — the
|
|
2224
|
+
* total guard for an untrusted storage read), or `undefined` if none is stored.
|
|
1931
2225
|
* - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
|
|
1932
2226
|
*
|
|
1933
2227
|
* UNLIKE a session store there is NO idle-TTL / eviction — a persisted conversation lives until an
|
|
1934
2228
|
* explicit `delete`. The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the
|
|
1935
|
-
*
|
|
2229
|
+
* method bijection with {@link ConversationStoreInterface}). Hydration stays a caller concern: a
|
|
1936
2230
|
* {@link import('../ConversationManager.js').ConversationManager} reads a snapshot back and rebuilds
|
|
1937
|
-
* the live conversation through the
|
|
2231
|
+
* the live conversation through the `snapshot` option (its `open` / `save`).
|
|
1938
2232
|
*
|
|
1939
2233
|
* @example
|
|
1940
2234
|
* ```ts
|
|
1941
|
-
* import { createConversation, createDatabaseConversationStore
|
|
2235
|
+
* import { createConversation, createDatabaseConversationStore } from '@orkestrel/agent'
|
|
2236
|
+
* import { createMemoryDriver } from '@orkestrel/database'
|
|
1942
2237
|
*
|
|
1943
2238
|
* const store = createDatabaseConversationStore(createMemoryDriver()) // a durable driver swaps in here
|
|
1944
2239
|
* const conversation = createConversation()
|
|
@@ -1951,7 +2246,7 @@ var Authority = class {
|
|
|
1951
2246
|
var DatabaseConversationStore = class {
|
|
1952
2247
|
#table;
|
|
1953
2248
|
/**
|
|
1954
|
-
*
|
|
2249
|
+
* Wraps a table as a conversation store.
|
|
1955
2250
|
*
|
|
1956
2251
|
* @param table - The {@link TableInterface} holding the snapshots — its row is the
|
|
1957
2252
|
* {@link ConversationSnapshotRow} `{ id; snapshot }` shape (the snapshot one opaque JSON column)
|
|
@@ -1959,20 +2254,20 @@ var DatabaseConversationStore = class {
|
|
|
1959
2254
|
constructor(table) {
|
|
1960
2255
|
this.#table = table;
|
|
1961
2256
|
}
|
|
1962
|
-
/**
|
|
2257
|
+
/** Resolves the persisted snapshot for `id`, narrowing the opaque JSON column back to a `ConversationSnapshot`. */
|
|
1963
2258
|
async get(id) {
|
|
1964
2259
|
const row = await this.#table.get(id);
|
|
1965
2260
|
if (row === void 0) return void 0;
|
|
1966
2261
|
return isConversationSnapshot(row.snapshot) ? row.snapshot : void 0;
|
|
1967
2262
|
}
|
|
1968
|
-
/**
|
|
2263
|
+
/** Inserts or replaces under the snapshot's OWN `id` (no separate id param) — the row is `{ id, snapshot }`. */
|
|
1969
2264
|
async set(snapshot) {
|
|
1970
2265
|
await this.#table.set({
|
|
1971
2266
|
id: snapshot.id,
|
|
1972
2267
|
snapshot
|
|
1973
2268
|
});
|
|
1974
2269
|
}
|
|
1975
|
-
/**
|
|
2270
|
+
/** Drops a snapshot by id; an absent id is a no-op (no throw). */
|
|
1976
2271
|
async delete(id) {
|
|
1977
2272
|
await this.#table.remove(id);
|
|
1978
2273
|
}
|
|
@@ -1980,14 +2275,14 @@ var DatabaseConversationStore = class {
|
|
|
1980
2275
|
//#endregion
|
|
1981
2276
|
//#region src/core/conversations/stores/MemoryConversationStore.ts
|
|
1982
2277
|
/**
|
|
1983
|
-
*
|
|
2278
|
+
* Implements the {@link ConversationStoreInterface} in memory — a process-lifetime `Map` of
|
|
1984
2279
|
* {@link ConversationSnapshot}s keyed by conversation id, the DEFAULT store
|
|
1985
2280
|
* {@link import('../../factories.js').createMemoryConversationStore} builds. The EXACT twin of
|
|
1986
2281
|
* {@link import('@orkestrel/workspace').MemoryWorkspaceStore}.
|
|
1987
2282
|
*
|
|
1988
2283
|
* @remarks
|
|
1989
|
-
* A plain `Map<string, ConversationSnapshot>`
|
|
1990
|
-
* self-contained JSON, so no encoding is needed for the memory tier
|
|
2284
|
+
* A plain `Map<string, ConversationSnapshot>` — the snapshot is already pure,
|
|
2285
|
+
* self-contained JSON, so no encoding is needed for the memory tier. Like the
|
|
1991
2286
|
* {@link import('@orkestrel/workspace').MemoryWorkspaceStore} it twins,
|
|
1992
2287
|
* there is NO idle-TTL and NO eviction: a persisted conversation lives until an explicit `delete`. A
|
|
1993
2288
|
* durable backend (JSON / SQLite / IndexedDB) swaps in through the SAME interface without touching
|
|
@@ -2000,14 +2295,14 @@ var DatabaseConversationStore = class {
|
|
|
2000
2295
|
* - **`set` inserts / replaces under the snapshot's OWN `id`** (no separate id param).
|
|
2001
2296
|
* - **`delete` drops a snapshot by id**; an absent id is a no-op (no throw).
|
|
2002
2297
|
*
|
|
2003
|
-
* The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the
|
|
2298
|
+
* The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the method
|
|
2004
2299
|
* bijection with {@link ConversationStoreInterface}). Hydration is a caller concern: a
|
|
2005
2300
|
* {@link import('../ConversationManager.js').ConversationManager} reads a snapshot back and rebuilds
|
|
2006
|
-
* the live conversation through the
|
|
2301
|
+
* the live conversation through the `snapshot` option (its `open` / `save`).
|
|
2007
2302
|
*
|
|
2008
2303
|
* @example
|
|
2009
2304
|
* ```ts
|
|
2010
|
-
* import { createConversation, createMemoryConversationStore } from '@
|
|
2305
|
+
* import { createConversation, createMemoryConversationStore } from '@orkestrel/agent'
|
|
2011
2306
|
*
|
|
2012
2307
|
* const store = createMemoryConversationStore()
|
|
2013
2308
|
* const conversation = createConversation()
|
|
@@ -2034,14 +2329,14 @@ var MemoryConversationStore = class {
|
|
|
2034
2329
|
//#endregion
|
|
2035
2330
|
//#region src/core/scopes/Scope.ts
|
|
2036
2331
|
/**
|
|
2037
|
-
*
|
|
2038
|
-
*
|
|
2039
|
-
* identity (an instruction's `name`, a tool's `name`, a workspace file's `path`).
|
|
2332
|
+
* Represents a named, immutable filter over a richer context's items — an optional
|
|
2333
|
+
* allow-list per category (`instructions` / `tools` / `files`), each keyed by that
|
|
2334
|
+
* category's identity (an instruction's `name`, a tool's `name`, a workspace file's `path`).
|
|
2040
2335
|
*
|
|
2041
2336
|
* @remarks
|
|
2042
2337
|
* - **A category list is three-way.** `undefined` ⇒ NO constraint on that category (all
|
|
2043
2338
|
* pass); `[]` ⇒ NONE pass; a non-empty list ⇒ only the listed keys pass. The build
|
|
2044
|
-
* step / loop apply this
|
|
2339
|
+
* step / loop apply this through `filterAllowList`.
|
|
2045
2340
|
* - **Immutable.** The `id` is minted at construction; every supplied list is COPIED in
|
|
2046
2341
|
* (so a later mutation of the caller's array can't leak in), and the lists are
|
|
2047
2342
|
* `readonly`. A `Scope` is never mutated after construction — `narrow` returns a NEW
|
|
@@ -2076,9 +2371,9 @@ var Scope = class Scope {
|
|
|
2076
2371
|
if (input.files !== void 0) this.files = [...input.files];
|
|
2077
2372
|
}
|
|
2078
2373
|
narrow(config) {
|
|
2079
|
-
const instructions =
|
|
2080
|
-
const tools =
|
|
2081
|
-
const files =
|
|
2374
|
+
const instructions = intersectKeys(this.instructions, config.instructions);
|
|
2375
|
+
const tools = intersectKeys(this.tools, config.tools);
|
|
2376
|
+
const files = intersectKeys(this.files, config.files);
|
|
2082
2377
|
return new Scope({
|
|
2083
2378
|
name: this.name,
|
|
2084
2379
|
...instructions === void 0 ? {} : { instructions },
|
|
@@ -2086,18 +2381,12 @@ var Scope = class Scope {
|
|
|
2086
2381
|
...files === void 0 ? {} : { files }
|
|
2087
2382
|
});
|
|
2088
2383
|
}
|
|
2089
|
-
static #intersect(parent, child) {
|
|
2090
|
-
if (parent === void 0) return child === void 0 ? void 0 : [...child];
|
|
2091
|
-
if (child === void 0) return [...parent];
|
|
2092
|
-
const allowed = new Set(parent);
|
|
2093
|
-
return child.filter((key) => allowed.has(key));
|
|
2094
|
-
}
|
|
2095
2384
|
};
|
|
2096
2385
|
//#endregion
|
|
2097
2386
|
//#region src/core/scopes/ScopeManager.ts
|
|
2098
2387
|
/**
|
|
2099
|
-
*
|
|
2100
|
-
*
|
|
2388
|
+
* Registers the named filters a richer context reuses — immutable {@link Scope}s keyed by
|
|
2389
|
+
* their minted `id`, in insertion order.
|
|
2101
2390
|
*
|
|
2102
2391
|
* @remarks
|
|
2103
2392
|
* - **Registry.** Scopes live in an insertion-ordered `Map` keyed by their minted `id`;
|
|
@@ -2106,9 +2395,9 @@ var Scope = class Scope {
|
|
|
2106
2395
|
* one up, and `scopes()` lists them in insertion order. (Unlike the name-keyed
|
|
2107
2396
|
* instruction registry, a scope's key is its minted `id`, so two scopes may share a
|
|
2108
2397
|
* `name`; `create` therefore always adds — it never overwrites.)
|
|
2109
|
-
* - **Removal.** `remove` drops one by id, or a batch
|
|
2110
|
-
* removed; `clear` empties the registry.
|
|
2111
|
-
* - **Observable
|
|
2398
|
+
* - **Removal.** `remove` drops one by id, or a batch — `true` only when EVERY supplied id
|
|
2399
|
+
* was removed; `clear` empties the registry.
|
|
2400
|
+
* - **Observable.** The owned {@link emitter} ({@link ScopeManagerEventMap}) carries
|
|
2112
2401
|
* `create` (the created scope) / `remove` (the id) / `clear`. Every event is emitted
|
|
2113
2402
|
* directly, strictly AFTER the map mutation completes; the emitter isolates a listener
|
|
2114
2403
|
* throw and routes it to its `error` handler (the `error` option), so a buggy observer can
|
|
@@ -2125,10 +2414,10 @@ var Scope = class Scope {
|
|
|
2125
2414
|
var ScopeManager = class {
|
|
2126
2415
|
#scopes = /* @__PURE__ */ new Map();
|
|
2127
2416
|
#emitter;
|
|
2128
|
-
constructor(
|
|
2417
|
+
constructor(options) {
|
|
2129
2418
|
this.#emitter = new Emitter({
|
|
2130
|
-
...on === void 0 ? {} : { on },
|
|
2131
|
-
...error === void 0 ? {} : { error }
|
|
2419
|
+
...options?.on === void 0 ? {} : { on: options.on },
|
|
2420
|
+
...options?.error === void 0 ? {} : { error: options.error }
|
|
2132
2421
|
});
|
|
2133
2422
|
}
|
|
2134
2423
|
get emitter() {
|
|
@@ -2151,8 +2440,8 @@ var ScopeManager = class {
|
|
|
2151
2440
|
}
|
|
2152
2441
|
remove(ids) {
|
|
2153
2442
|
if (isArray(ids)) {
|
|
2154
|
-
let removed =
|
|
2155
|
-
for (const id of ids) if (this.#delete(id)) removed =
|
|
2443
|
+
let removed = true;
|
|
2444
|
+
for (const id of ids) if (!this.#delete(id)) removed = false;
|
|
2156
2445
|
return removed;
|
|
2157
2446
|
}
|
|
2158
2447
|
return this.#delete(ids);
|
|
@@ -2170,10 +2459,10 @@ var ScopeManager = class {
|
|
|
2170
2459
|
//#endregion
|
|
2171
2460
|
//#region src/core/ThinkSplitter.ts
|
|
2172
2461
|
/**
|
|
2173
|
-
*
|
|
2174
|
-
*
|
|
2175
|
-
*
|
|
2176
|
-
*
|
|
2462
|
+
* Feeds raw content deltas through a tiny stream-stateful state machine that routes
|
|
2463
|
+
* everything inside a `<think>…</think>` span to `thinking` and returns everything outside
|
|
2464
|
+
* it as clean content, so a provider yields ONLY the answer and surfaces the reasoning as
|
|
2465
|
+
* {@link import('./types.js').ProviderResult.thinking}.
|
|
2177
2466
|
*
|
|
2178
2467
|
* @remarks
|
|
2179
2468
|
* - **Cross-chunk tags.** A tag may arrive split across wire deltas (`'<thi'` then
|
|
@@ -2189,7 +2478,7 @@ var ScopeManager = class {
|
|
|
2189
2478
|
* prefix cannot be recalled, so `content` is the authoritative accumulation). The
|
|
2190
2479
|
* rule is ONE-SHOT: after any tag event a bare `</think>` is plain text.
|
|
2191
2480
|
* - **Multiple spans** accumulate onto `thinking` in stream order. A nested-looking
|
|
2192
|
-
* `<think>` inside an open span is
|
|
2481
|
+
* `<think>` inside an open span is thinking text (no nesting is tracked — the
|
|
2193
2482
|
* first `</think>` closes the span), matching how the models emit it.
|
|
2194
2483
|
* - **Unclosed span at stream end.** `flush()` routes the open span's tail (including
|
|
2195
2484
|
* any held partial close tag) to `thinking` — a cut-off model was still reasoning.
|
|
@@ -2288,28 +2577,28 @@ var ThinkSplitter = class {
|
|
|
2288
2577
|
//#endregion
|
|
2289
2578
|
//#region src/core/factories.ts
|
|
2290
2579
|
/**
|
|
2291
|
-
*
|
|
2580
|
+
* Creates a conversation — a {@link ConversationInterface} grouping messages above a flat
|
|
2292
2581
|
* message store it OWNS DIRECTLY, with compaction into summarized sections, a regenerated
|
|
2293
2582
|
* rollup `summary`, on-demand `rehydrate`, and substring `search`, driven by a
|
|
2294
|
-
* provider-agnostic {@link
|
|
2583
|
+
* provider-agnostic {@link ConversationSummaryHandler} seam.
|
|
2295
2584
|
*
|
|
2296
2585
|
* @remarks
|
|
2297
2586
|
* Append turns through the conversation's own `add` (the live tail it owns); `view()` is the model input
|
|
2298
2587
|
* (each section as a summary message, then the live tail). `compact()` folds the older live
|
|
2299
|
-
* messages into a summarized {@link
|
|
2588
|
+
* messages into a summarized {@link Section} and regenerates the rollup — it REQUIRES
|
|
2300
2589
|
* a `summarize` (omitted ⇒ `compact()` throws a `ConversationError`); `keep` retains a recent
|
|
2301
2590
|
* tail (default `DEFAULT_CONVERSATION_KEEP` — fold ALL). `rehydrate(id)` / `search(query)` read
|
|
2302
2591
|
* the retained originals. Observable (`emitter` — `compact` / `summary` / `rehydrate`), wired
|
|
2303
|
-
*
|
|
2304
|
-
* its `error` handler (the `error` option
|
|
2592
|
+
* through the reserved `on` option; the emitter isolates a listener throw and routes it to
|
|
2593
|
+
* its `error` handler (the `error` option), so it can never corrupt a compaction.
|
|
2305
2594
|
*
|
|
2306
2595
|
* @param options - Optional `id` / `on` hooks + the `summarize` seam + `keep` (see {@link ConversationOptions})
|
|
2307
2596
|
* @returns A working {@link ConversationInterface}
|
|
2308
2597
|
*
|
|
2309
2598
|
* @example
|
|
2310
2599
|
* ```ts
|
|
2311
|
-
* import type { ProviderInterface } from '@
|
|
2312
|
-
* import { createConversation } from '@
|
|
2600
|
+
* import type { ProviderInterface } from '@orkestrel/agent'
|
|
2601
|
+
* import { createConversation } from '@orkestrel/agent'
|
|
2313
2602
|
*
|
|
2314
2603
|
* declare const provider: ProviderInterface // any concrete implementation supplied by the host app
|
|
2315
2604
|
* const conversation = createConversation({
|
|
@@ -2326,9 +2615,10 @@ function createConversation(options) {
|
|
|
2326
2615
|
return new Conversation(options);
|
|
2327
2616
|
}
|
|
2328
2617
|
/**
|
|
2329
|
-
*
|
|
2618
|
+
* Creates a conversation registry — a {@link ConversationManagerInterface} holding
|
|
2330
2619
|
* {@link ConversationInterface}s keyed by their `id` (in insertion order) WITH an active pointer:
|
|
2331
|
-
* the
|
|
2620
|
+
* the id-keyed store over the conversation layer plus the `active` / `switch` seam the context
|
|
2621
|
+
* renders.
|
|
2332
2622
|
*
|
|
2333
2623
|
* @remarks
|
|
2334
2624
|
* Starts empty; `add(input?)` mints a {@link ConversationInterface} (its `id` from the input
|
|
@@ -2337,8 +2627,9 @@ function createConversation(options) {
|
|
|
2337
2627
|
* AUTO-ACTIVATES the FIRST one (a registry with conversations always has one `active`); a later
|
|
2338
2628
|
* `add` leaves `active` unchanged. `switch(id)` re-points `active` (an unknown `id` returns
|
|
2339
2629
|
* `undefined`, leaving `active` unchanged — lenient, never throws); `conversation(id)` /
|
|
2340
|
-
* `conversations()` look up; `remove` (one or a batch
|
|
2341
|
-
* clears `active` if it was
|
|
2630
|
+
* `conversations()` look up; `remove` (one or a batch) reports `true` only when every supplied id
|
|
2631
|
+
* was removed AND clears `active` if it was a removed one; `clear` empties it and clears `active`.
|
|
2632
|
+
* Event-free
|
|
2342
2633
|
* (each conversation owns its own observable `emitter`). A conversation created with NEITHER a
|
|
2343
2634
|
* manager default nor a per-`add` `summarize` cannot `compact` (it throws a `ConversationError`).
|
|
2344
2635
|
*
|
|
@@ -2347,7 +2638,7 @@ function createConversation(options) {
|
|
|
2347
2638
|
*
|
|
2348
2639
|
* @example
|
|
2349
2640
|
* ```ts
|
|
2350
|
-
* import { createConversationManager } from '@
|
|
2641
|
+
* import { createConversationManager } from '@orkestrel/agent'
|
|
2351
2642
|
*
|
|
2352
2643
|
* const conversations = createConversationManager({ summarize: async (m) => `recap of ${m.length}` })
|
|
2353
2644
|
* const chat = conversations.add() // auto-activates — conversations.active === chat
|
|
@@ -2358,7 +2649,7 @@ function createConversationManager(options) {
|
|
|
2358
2649
|
return new ConversationManager(options);
|
|
2359
2650
|
}
|
|
2360
2651
|
/**
|
|
2361
|
-
*
|
|
2652
|
+
* Creates the in-memory conversation store — a {@link ConversationStoreInterface} backed by a
|
|
2362
2653
|
* process-lifetime `Map` of {@link import('./types.js').ConversationSnapshot}s keyed by conversation
|
|
2363
2654
|
* id, the DEFAULT backing for the durable {@link ConversationManagerInterface.open} /
|
|
2364
2655
|
* {@link ConversationManagerInterface.save} seam. The exact twin of
|
|
@@ -2373,14 +2664,14 @@ function createConversationManager(options) {
|
|
|
2373
2664
|
* {@link createDatabaseConversationStore} (the snapshot as one opaque JSON column over a `databases`
|
|
2374
2665
|
* table) — for a DURABLE store pass it a JSON / SQLite / IndexedDB driver, and it swaps in WITHOUT
|
|
2375
2666
|
* touching the manager or the conversation. Hydration stays a manager concern: read a snapshot back
|
|
2376
|
-
* and rebuild the live conversation through the
|
|
2667
|
+
* and rebuild the live conversation through the `snapshot` option (re-supplying the live
|
|
2377
2668
|
* `summarize` / `keep`).
|
|
2378
2669
|
*
|
|
2379
2670
|
* @returns A memory-backed {@link ConversationStoreInterface}
|
|
2380
2671
|
*
|
|
2381
2672
|
* @example
|
|
2382
2673
|
* ```ts
|
|
2383
|
-
* import { createConversationManager, createMemoryConversationStore } from '@
|
|
2674
|
+
* import { createConversationManager, createMemoryConversationStore } from '@orkestrel/agent'
|
|
2384
2675
|
*
|
|
2385
2676
|
* const store = createMemoryConversationStore()
|
|
2386
2677
|
* const manager = createConversationManager({ store })
|
|
@@ -2393,7 +2684,7 @@ function createMemoryConversationStore() {
|
|
|
2393
2684
|
return new MemoryConversationStore();
|
|
2394
2685
|
}
|
|
2395
2686
|
/**
|
|
2396
|
-
*
|
|
2687
|
+
* Creates a {@link DatabaseConversationStore} over any {@link DriverInterface} — the durable,
|
|
2397
2688
|
* driver-pluggable backing for the conversation persistence seam, the opt-in twin of
|
|
2398
2689
|
* {@link createMemoryConversationStore}. The exact twin of
|
|
2399
2690
|
* {@link import('@orkestrel/workspace').createDatabaseWorkspaceStore}.
|
|
@@ -2405,7 +2696,7 @@ function createMemoryConversationStore() {
|
|
|
2405
2696
|
* {@link import('@orkestrel/workspace').createDatabaseWorkspaceStore} stores its snapshot. The
|
|
2406
2697
|
* snapshot is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless
|
|
2407
2698
|
* AND keeps the row type FLAT (the column reads back as `unknown`, narrowed on `get` by
|
|
2408
|
-
* {@link import('./
|
|
2699
|
+
* {@link import('./validators.js').isConversationSnapshot}). The `driver` DEFAULTS to
|
|
2409
2700
|
* {@link createMemoryDriver}, so the store ALSO works in memory out of the box; pass a server
|
|
2410
2701
|
* `createJSONDriver` / `createSQLiteDriver` (or a browser IndexedDB driver) for a persistent one —
|
|
2411
2702
|
* the durability is the driver's job, the store engine is shared. It swaps in behind
|
|
@@ -2416,7 +2707,8 @@ function createMemoryConversationStore() {
|
|
|
2416
2707
|
*
|
|
2417
2708
|
* @example
|
|
2418
2709
|
* ```ts
|
|
2419
|
-
* import { createConversationManager, createDatabaseConversationStore
|
|
2710
|
+
* import { createConversationManager, createDatabaseConversationStore } from '@orkestrel/agent'
|
|
2711
|
+
* import { createMemoryDriver } from '@orkestrel/database'
|
|
2420
2712
|
*
|
|
2421
2713
|
* const store = createDatabaseConversationStore(createMemoryDriver()) // a durable driver swaps in here
|
|
2422
2714
|
* const manager = createConversationManager({ store })
|
|
@@ -2436,7 +2728,7 @@ function createDatabaseConversationStore(driver = createMemoryDriver()) {
|
|
|
2436
2728
|
}).table("conversations"));
|
|
2437
2729
|
}
|
|
2438
2730
|
/**
|
|
2439
|
-
*
|
|
2731
|
+
* Creates an instruction — an immutable {@link InstructionInterface} (a named directive)
|
|
2440
2732
|
* from its `name` / `content` and optional `priority`, the `id` minted at construction.
|
|
2441
2733
|
*
|
|
2442
2734
|
* @remarks
|
|
@@ -2450,7 +2742,7 @@ function createDatabaseConversationStore(driver = createMemoryDriver()) {
|
|
|
2450
2742
|
*
|
|
2451
2743
|
* @example
|
|
2452
2744
|
* ```ts
|
|
2453
|
-
* import { createInstruction } from '@
|
|
2745
|
+
* import { createInstruction } from '@orkestrel/agent'
|
|
2454
2746
|
*
|
|
2455
2747
|
* const instruction = createInstruction({ name: 'tone', content: 'Be concise.', priority: 5 })
|
|
2456
2748
|
* ```
|
|
@@ -2459,28 +2751,28 @@ function createInstruction(input) {
|
|
|
2459
2751
|
return new Instruction(input);
|
|
2460
2752
|
}
|
|
2461
2753
|
/**
|
|
2462
|
-
*
|
|
2754
|
+
* Creates an instruction registry — an {@link InstructionManagerInterface} holding
|
|
2463
2755
|
* immutable instructions keyed by `name`, listed by descending `priority`.
|
|
2464
2756
|
*
|
|
2465
2757
|
* @remarks
|
|
2466
|
-
* Starts empty; `add` (one or a batch
|
|
2758
|
+
* Starts empty; `add` (one or a batch) MINTS each `id` and OVERWRITES a same-name
|
|
2467
2759
|
* instruction (last write wins); `instructions()` lists them sorted by descending
|
|
2468
|
-
* `priority` (stable for ties); `
|
|
2469
|
-
* context renders an instructions block with; `remove` (one or a batch) reports
|
|
2470
|
-
*
|
|
2471
|
-
* ({@link import('./types.js').InstructionManagerEventMap}) wired
|
|
2472
|
-
* option
|
|
2473
|
-
* (the `error` option
|
|
2760
|
+
* `priority` (stable for ties); `open` / `render` are the build contract a richer
|
|
2761
|
+
* context renders an instructions block with; `remove` (one or a batch) reports `true` only
|
|
2762
|
+
* when every supplied name was removed; `clear` empties it. Carries an observable `emitter`
|
|
2763
|
+
* ({@link import('./types.js').InstructionManagerEventMap}) wired through the reserved `on`
|
|
2764
|
+
* option; the emitter isolates a listener throw and routes it to its `error` handler
|
|
2765
|
+
* (the `error` option), so it can never corrupt a mutation. An optional `format`
|
|
2474
2766
|
* override is the manager-options level of the `AgentContext` build cascade (consulted by
|
|
2475
|
-
* `
|
|
2476
|
-
* `InstructionInput.
|
|
2767
|
+
* `open` / `render`, beating the provider default + built-in; a per-item
|
|
2768
|
+
* `InstructionInput.override` still beats it).
|
|
2477
2769
|
*
|
|
2478
2770
|
* @param options - Optional `on` hooks + a `format` override (see {@link InstructionManagerOptions})
|
|
2479
2771
|
* @returns An empty {@link InstructionManagerInterface}
|
|
2480
2772
|
*
|
|
2481
2773
|
* @example
|
|
2482
2774
|
* ```ts
|
|
2483
|
-
* import { createInstructionManager } from '@
|
|
2775
|
+
* import { createInstructionManager } from '@orkestrel/agent'
|
|
2484
2776
|
*
|
|
2485
2777
|
* const instructions = createInstructionManager()
|
|
2486
2778
|
* instructions.add({ name: 'tone', content: 'Be concise.', priority: 5 })
|
|
@@ -2490,8 +2782,8 @@ function createInstructionManager(options) {
|
|
|
2490
2782
|
return new InstructionManager(options);
|
|
2491
2783
|
}
|
|
2492
2784
|
/**
|
|
2493
|
-
*
|
|
2494
|
-
*
|
|
2785
|
+
* Creates a named scope — an immutable {@link ScopeInterface} from its `name` and its
|
|
2786
|
+
* per-category allow-lists, the `id` minted at construction.
|
|
2495
2787
|
*
|
|
2496
2788
|
* @remarks
|
|
2497
2789
|
* Each list is THREE-WAY: `undefined` ⇒ NO constraint on that category (all pass), `[]` ⇒
|
|
@@ -2499,13 +2791,13 @@ function createInstructionManager(options) {
|
|
|
2499
2791
|
* tighter child by set-INTERSECTION (an `undefined` side imposing no constraint). Stored
|
|
2500
2792
|
* immutable — never mutated after creation (`narrow` returns a new scope).
|
|
2501
2793
|
*
|
|
2502
|
-
* @param input - `name` (required) and the optional `instructions` / `tools` / `
|
|
2503
|
-
*
|
|
2794
|
+
* @param input - `name` (required) and the optional `instructions` / `tools` / `files`
|
|
2795
|
+
* allow-lists (see {@link ScopeInput})
|
|
2504
2796
|
* @returns A working {@link ScopeInterface}
|
|
2505
2797
|
*
|
|
2506
2798
|
* @example
|
|
2507
2799
|
* ```ts
|
|
2508
|
-
* import { createScope } from '@
|
|
2800
|
+
* import { createScope } from '@orkestrel/agent'
|
|
2509
2801
|
*
|
|
2510
2802
|
* const reader = createScope({ name: 'reader', tools: ['search', 'read'] })
|
|
2511
2803
|
* reader.narrow({ tools: ['read', 'write'] }).tools // ['read'] — intersection tightens
|
|
@@ -2515,33 +2807,34 @@ function createScope(input) {
|
|
|
2515
2807
|
return new Scope(input);
|
|
2516
2808
|
}
|
|
2517
2809
|
/**
|
|
2518
|
-
*
|
|
2810
|
+
* Creates a scope registry — a {@link ScopeManagerInterface} holding immutable scopes keyed
|
|
2519
2811
|
* by their minted `id`, in insertion order.
|
|
2520
2812
|
*
|
|
2521
2813
|
* @remarks
|
|
2522
2814
|
* Starts empty; `create` mints each scope's `id` and stores it (keyed by `id`, so it
|
|
2523
2815
|
* always adds — two scopes may share a `name`); `scopes()` lists them in insertion order;
|
|
2524
|
-
* `remove` (one or a batch
|
|
2525
|
-
* Carries an observable `emitter`
|
|
2526
|
-
*
|
|
2527
|
-
*
|
|
2816
|
+
* `remove` (one or a batch) reports `true` only when every supplied id was removed; `clear`
|
|
2817
|
+
* empties it. Carries an observable `emitter`
|
|
2818
|
+
* ({@link import('./types.js').ScopeManagerEventMap}) wired through the reserved `on`
|
|
2819
|
+
* option; the emitter isolates a listener throw and routes it to its `error` handler
|
|
2820
|
+
* (the `error` option), so it can never corrupt a mutation.
|
|
2528
2821
|
*
|
|
2529
2822
|
* @param options - Optional `on` hooks (see {@link ScopeManagerOptions})
|
|
2530
2823
|
* @returns An empty {@link ScopeManagerInterface}
|
|
2531
2824
|
*
|
|
2532
2825
|
* @example
|
|
2533
2826
|
* ```ts
|
|
2534
|
-
* import { createScopeManager } from '@
|
|
2827
|
+
* import { createScopeManager } from '@orkestrel/agent'
|
|
2535
2828
|
*
|
|
2536
2829
|
* const scopes = createScopeManager()
|
|
2537
2830
|
* const reader = scopes.create({ name: 'reader', tools: ['search'] })
|
|
2538
2831
|
* ```
|
|
2539
2832
|
*/
|
|
2540
2833
|
function createScopeManager(options) {
|
|
2541
|
-
return new ScopeManager(options
|
|
2834
|
+
return new ScopeManager(options);
|
|
2542
2835
|
}
|
|
2543
2836
|
/**
|
|
2544
|
-
*
|
|
2837
|
+
* Creates a richer turn context — an {@link AgentContextInterface} assembling a provider
|
|
2545
2838
|
* request from the optional system prompt, the instruction registry, the workspace registry,
|
|
2546
2839
|
* the conversation store, the tool registry, and the active scope.
|
|
2547
2840
|
*
|
|
@@ -2549,11 +2842,11 @@ function createScopeManager(options) {
|
|
|
2549
2842
|
* `system` is the optional system prompt; `tools` / `instructions` / `workspaces` are pre-built
|
|
2550
2843
|
* managers to reuse (empty ones are created when omitted, so `context.workspaces` is ALWAYS
|
|
2551
2844
|
* present); `scope` is the initial active filter (`undefined` ⇒ no filtering, changeable afterwards
|
|
2552
|
-
*
|
|
2845
|
+
* through `context.apply(...)`). The `messages` store is always fresh. `build()` folds the scoped
|
|
2553
2846
|
* instructions — PLUS the ACTIVE workspace's scope-filtered text files (fenced) — into ONE leading
|
|
2554
2847
|
* `system` message and appends the scoped conversation (attaching the active workspace's
|
|
2555
|
-
* scope-filtered image files' `
|
|
2556
|
-
* workspace is the SOLE document/image context. Tools are advertised STRUCTURALLY (
|
|
2848
|
+
* scope-filtered image files' `base64` payload to the last user message), built fresh each call; the active
|
|
2849
|
+
* workspace is the SOLE document/image context. Tools are advertised STRUCTURALLY (through
|
|
2557
2850
|
* `tools.definitions()`, scope-filtered by the loop), never serialized into the prompt.
|
|
2558
2851
|
*
|
|
2559
2852
|
* @param options - Optional `system` / `tools` / `instructions` / `workspaces` / `scope`
|
|
@@ -2562,7 +2855,7 @@ function createScopeManager(options) {
|
|
|
2562
2855
|
*
|
|
2563
2856
|
* @example
|
|
2564
2857
|
* ```ts
|
|
2565
|
-
* import { createAgentContext } from '@
|
|
2858
|
+
* import { createAgentContext } from '@orkestrel/agent'
|
|
2566
2859
|
*
|
|
2567
2860
|
* const context = createAgentContext({ system: 'You are concise.' })
|
|
2568
2861
|
* context.instructions.add({ name: 'tone', content: 'Be terse.' })
|
|
@@ -2574,7 +2867,7 @@ function createAgentContext(options) {
|
|
|
2574
2867
|
return new AgentContext(options);
|
|
2575
2868
|
}
|
|
2576
2869
|
/**
|
|
2577
|
-
*
|
|
2870
|
+
* Creates an agent loop — an {@link AgentInterface} composing a
|
|
2578
2871
|
* {@link ProviderInterface}, its {@link AgentContextInterface}, and a tool registry
|
|
2579
2872
|
* into a bounded context → provider → tools → repeat turn, exposed as a one-shot
|
|
2580
2873
|
* `generate` and a live `stream`.
|
|
@@ -2582,11 +2875,11 @@ function createAgentContext(options) {
|
|
|
2582
2875
|
* @remarks
|
|
2583
2876
|
* One private loop drives the turn; `generate` DRAINS the same stream `stream`
|
|
2584
2877
|
* exposes, so they can never diverge. Each turn is bounded by one cancel folded from
|
|
2585
|
-
* `signal` + `timeout` + `budget` (
|
|
2878
|
+
* `signal` + `timeout` + `budget` (through `AbortSignal.any`) — any trip (or `abort()`)
|
|
2586
2879
|
* commits a PARTIAL result (the stream's `result` RESOLVES on a cancel, rejects only
|
|
2587
2880
|
* on a genuine provider / tool error). The `scheduler` paces between turns; tool
|
|
2588
2881
|
* iteration is capped at `limit` (default `DEFAULT_AGENT_LIMIT`). Tools are advertised
|
|
2589
|
-
* structurally
|
|
2882
|
+
* structurally through `context.tools.definitions()`. Two observation surfaces: the
|
|
2590
2883
|
* {@link AgentChunk} stream (pull — per-token content) and a typed `emitter` (push —
|
|
2591
2884
|
* lifecycle + `usage` / `tool` / `deny` for fire-and-forget observers).
|
|
2592
2885
|
*
|
|
@@ -2597,8 +2890,9 @@ function createAgentContext(options) {
|
|
|
2597
2890
|
*
|
|
2598
2891
|
* @example
|
|
2599
2892
|
* ```ts
|
|
2600
|
-
* import type { ProviderInterface } from '@
|
|
2601
|
-
* import { createAgent
|
|
2893
|
+
* import type { ProviderInterface } from '@orkestrel/agent'
|
|
2894
|
+
* import { createAgent } from '@orkestrel/agent'
|
|
2895
|
+
* import { createTokenBudget } from '@orkestrel/budget'
|
|
2602
2896
|
*
|
|
2603
2897
|
* declare const provider: ProviderInterface // any concrete implementation supplied by the host app
|
|
2604
2898
|
* const agent = createAgent(provider, {
|
|
@@ -2609,7 +2903,7 @@ function createAgentContext(options) {
|
|
|
2609
2903
|
*
|
|
2610
2904
|
* const stream = agent.stream()
|
|
2611
2905
|
* for await (const chunk of stream.events) {
|
|
2612
|
-
* if (chunk.
|
|
2906
|
+
* if (chunk.category === 'token') process.stdout.write(chunk.content)
|
|
2613
2907
|
* }
|
|
2614
2908
|
* const result = await stream.result // { content, usage?, partial }
|
|
2615
2909
|
* ```
|
|
@@ -2618,7 +2912,7 @@ function createAgent(provider, options) {
|
|
|
2618
2912
|
return new Agent(provider, options);
|
|
2619
2913
|
}
|
|
2620
2914
|
/**
|
|
2621
|
-
*
|
|
2915
|
+
* Creates a stream-stateful `<think>` separator — a {@link ThinkSplitterInterface} that
|
|
2622
2916
|
* splits a thinking model's in-content `<think>…</think>` reasoning spans away from
|
|
2623
2917
|
* the answer, delta by delta, so a provider yields ONLY clean content and surfaces the
|
|
2624
2918
|
* accumulated reasoning as {@link import('./types.js').ProviderResult.thinking}.
|
|
@@ -2629,14 +2923,14 @@ function createAgent(provider, options) {
|
|
|
2629
2923
|
* partial open tag that never completed returns as final content; an UNCLOSED think
|
|
2630
2924
|
* span lands on `thinking`). Tags split ACROSS deltas are held back until
|
|
2631
2925
|
* disambiguated, multiple spans accumulate in order, and a nested-looking `<think>`
|
|
2632
|
-
* inside an open span is
|
|
2926
|
+
* inside an open span is thinking text. One splitter serves ONE stream — create
|
|
2633
2927
|
* a fresh one per provider call.
|
|
2634
2928
|
*
|
|
2635
2929
|
* @returns A fresh {@link ThinkSplitterInterface} (state empty, outside any span)
|
|
2636
2930
|
*
|
|
2637
2931
|
* @example
|
|
2638
2932
|
* ```ts
|
|
2639
|
-
* import { createThinkSplitter } from '@
|
|
2933
|
+
* import { createThinkSplitter } from '@orkestrel/agent'
|
|
2640
2934
|
*
|
|
2641
2935
|
* const splitter = createThinkSplitter()
|
|
2642
2936
|
* const clean = splitter.split('<think>plan the answer</think>Here it is.')
|
|
@@ -2648,7 +2942,36 @@ function createThinkSplitter() {
|
|
|
2648
2942
|
return new ThinkSplitter();
|
|
2649
2943
|
}
|
|
2650
2944
|
/**
|
|
2651
|
-
*
|
|
2945
|
+
* Creates an empty unbounded async channel — a {@link ChannelInterface} a producer writes
|
|
2946
|
+
* values into (`push`) and ends (`close` / `fail`) regardless of consumption, while a
|
|
2947
|
+
* consumer reads them back live through `drain`.
|
|
2948
|
+
*
|
|
2949
|
+
* @remarks
|
|
2950
|
+
* Write and read are decoupled, so the producer never waits for a consumer: an agent's eager
|
|
2951
|
+
* pump writes each chunk into one, which is why a run's `result` settles whether or not the
|
|
2952
|
+
* live events are drained. A value pushed at an already-parked consumer is delivered, buffered
|
|
2953
|
+
* values are yielded before the end is reported, and the FIRST failure wins.
|
|
2954
|
+
*
|
|
2955
|
+
* @typeParam T - The value type the channel carries
|
|
2956
|
+
* @returns A fresh, empty {@link ChannelInterface}
|
|
2957
|
+
*
|
|
2958
|
+
* @example
|
|
2959
|
+
* ```ts
|
|
2960
|
+
* import { createChannel } from '@orkestrel/agent'
|
|
2961
|
+
*
|
|
2962
|
+
* const channel = createChannel<number>()
|
|
2963
|
+
* channel.push(1)
|
|
2964
|
+
* channel.close()
|
|
2965
|
+
* for await (const value of channel.drain()) {
|
|
2966
|
+
* value // 1
|
|
2967
|
+
* }
|
|
2968
|
+
* ```
|
|
2969
|
+
*/
|
|
2970
|
+
function createChannel() {
|
|
2971
|
+
return new Channel();
|
|
2972
|
+
}
|
|
2973
|
+
/**
|
|
2974
|
+
* Creates a policy gate — an {@link AuthorityInterface} the agent loop consults before
|
|
2652
2975
|
* each tool call runs, evaluating the ordered rules first-match-wins and falling back
|
|
2653
2976
|
* to the configured default when none match.
|
|
2654
2977
|
*
|
|
@@ -2658,16 +2981,16 @@ function createThinkSplitter() {
|
|
|
2658
2981
|
* `fallback` decides; it defaults to `{ zone: DEFAULT_AUTHORITY_ZONE, allowed: true }`
|
|
2659
2982
|
* (allow-unmatched — a rules list of denials acts as a DENYLIST). Pass an
|
|
2660
2983
|
* `allowed: false` `fallback` to flip the gate to deny-by-default (an ALLOWLIST). Wire
|
|
2661
|
-
* the result into `createAgent`
|
|
2984
|
+
* the result into `createAgent` through `AgentOptions.authority`: a denied call is fed back
|
|
2662
2985
|
* to the model as a denial `ToolResult` (not executed, no budget cost), so the model
|
|
2663
|
-
* can react. Synchronous
|
|
2986
|
+
* can react. Synchronous — `evaluate` returns the verdict directly.
|
|
2664
2987
|
*
|
|
2665
2988
|
* @param options - Optional `rules` (ordered) and `fallback` (see {@link AuthorityOptions})
|
|
2666
2989
|
* @returns A working {@link AuthorityInterface}
|
|
2667
2990
|
*
|
|
2668
2991
|
* @example
|
|
2669
2992
|
* ```ts
|
|
2670
|
-
* import { createAgent, createAuthority } from '@
|
|
2993
|
+
* import { createAgent, createAuthority } from '@orkestrel/agent'
|
|
2671
2994
|
*
|
|
2672
2995
|
* // Deny the `delete` tool, allow everything else.
|
|
2673
2996
|
* const authority = createAuthority({
|
|
@@ -2680,16 +3003,17 @@ function createAuthority(options) {
|
|
|
2680
3003
|
return new Authority(options);
|
|
2681
3004
|
}
|
|
2682
3005
|
/**
|
|
2683
|
-
*
|
|
3006
|
+
* Creates an agent registry — an {@link AgentRegistryInterface} holding the named pools of
|
|
2684
3007
|
* live, non-serializable pieces (providers, tools, authorities, schedulers) that a
|
|
2685
3008
|
* serializable {@link AgentJobInput}'s names resolve against, and `build`ing a seeded,
|
|
2686
3009
|
* signal-wired {@link AgentInterface} from a job.
|
|
2687
3010
|
*
|
|
2688
3011
|
* @remarks
|
|
2689
3012
|
* `providers` is required; `tools` / `authorities` / `schedulers` are optional pools.
|
|
2690
|
-
* The accessors (`provider` / `tool` / `authority` / `scheduler`) THROW
|
|
2691
|
-
*
|
|
2692
|
-
*
|
|
3013
|
+
* The accessors (`provider` / `tool` / `authority` / `scheduler`) THROW an
|
|
3014
|
+
* {@link import('./errors.js').AgentError} carrying `code: 'REGISTRY'` and the message
|
|
3015
|
+
* `unknown <category>: <name>` on an unregistered name — a misconfigured or crash-restored
|
|
3016
|
+
* job fails loudly rather than running with a missing dependency. `build(input, signal)`
|
|
2693
3017
|
* resolves the names, rebuilds the token budget from its ceiling, seeds the agent's
|
|
2694
3018
|
* context with the job's messages, and threads `signal` so a queue / runner abort
|
|
2695
3019
|
* propagates. This is the bridge that makes durable, serializable agent jobs runnable.
|
|
@@ -2699,8 +3023,8 @@ function createAuthority(options) {
|
|
|
2699
3023
|
*
|
|
2700
3024
|
* @example
|
|
2701
3025
|
* ```ts
|
|
2702
|
-
* import { createAgentRegistry } from '@
|
|
2703
|
-
* import type { ProviderInterface } from '@
|
|
3026
|
+
* import { createAgentRegistry } from '@orkestrel/agent'
|
|
3027
|
+
* import type { ProviderInterface } from '@orkestrel/agent'
|
|
2704
3028
|
* import { createTool } from '@orkestrel/tool'
|
|
2705
3029
|
*
|
|
2706
3030
|
* declare const provider: ProviderInterface // any concrete implementation supplied by the host app
|
|
@@ -2715,7 +3039,7 @@ function createAgentRegistry(options) {
|
|
|
2715
3039
|
return new AgentRegistry(options);
|
|
2716
3040
|
}
|
|
2717
3041
|
/**
|
|
2718
|
-
*
|
|
3042
|
+
* Creates a durable, bounded-concurrency agent-job queue — a {@link QueueInterface} over
|
|
2719
3043
|
* serializable {@link AgentJobInput}s that COMPOSES `createQueue`: each job is rehydrated
|
|
2720
3044
|
* through the `registry` into a live {@link AgentInterface}, run to its {@link AgentResult},
|
|
2721
3045
|
* and subjected to the partial-as-configurable-failure policy.
|
|
@@ -2723,46 +3047,47 @@ function createAgentRegistry(options) {
|
|
|
2723
3047
|
* @remarks
|
|
2724
3048
|
* - **Composes the substrate (no new engine).** The handler is the only new logic;
|
|
2725
3049
|
* bounded `concurrency`, `retries`, the per-attempt `timeout`, and durable persistence
|
|
2726
|
-
*
|
|
3050
|
+
* through `store` (+ `restore()` after a crash) are all the backing Queue's. `enqueue`
|
|
2727
3051
|
* returns a per-job promise.
|
|
2728
3052
|
* - **Durable + serializable.** Because `AgentJobInput` is JSON-serializable, a `store`
|
|
2729
|
-
* (
|
|
3053
|
+
* (for example `createMemoryQueueStore` / `createDatabaseQueueStore`) persists outstanding
|
|
2730
3054
|
* jobs; `restore()` re-enqueues them after a restart and the `registry` rehydrates the
|
|
2731
3055
|
* live pieces from the names — so a job survives a crash.
|
|
2732
3056
|
* - **Partial policy.** A partial result THROWS an
|
|
2733
3057
|
* {@link import('./errors.js').AgentJobError} by default, so a job cancelled by its
|
|
2734
|
-
* attempt deadline / a queue abort RETRIES while attempts remain; `
|
|
3058
|
+
* attempt deadline / a queue abort RETRIES while attempts remain; `partial: true`
|
|
2735
3059
|
* resolves the partial as success instead.
|
|
2736
|
-
* - **Cancellation threads through.** The handler passes `
|
|
3060
|
+
* - **Cancellation threads through.** The handler passes `context.signal` into
|
|
2737
3061
|
* `registry.build`, so a queue `abort()` or a per-attempt timeout cancels the in-flight
|
|
2738
3062
|
* agent (which commits a partial → throws → retries / fails per policy).
|
|
2739
3063
|
*
|
|
2740
|
-
* @param options - The `registry`, the `
|
|
3064
|
+
* @param options - The `registry`, the `partial` policy, and the substrate knobs
|
|
2741
3065
|
* (`concurrency` / `retries` / `timeout` / `store`) (see {@link AgentQueueOptions})
|
|
2742
3066
|
* @returns A {@link QueueInterface} of {@link AgentJobInput} → {@link AgentResult}
|
|
2743
3067
|
*
|
|
2744
3068
|
* @example
|
|
2745
3069
|
* ```ts
|
|
2746
|
-
* import { createAgentQueue, createAgentRegistry
|
|
3070
|
+
* import { createAgentQueue, createAgentRegistry } from '@orkestrel/agent'
|
|
3071
|
+
* import { createMemoryQueueStore } from '@orkestrel/queue'
|
|
2747
3072
|
*
|
|
2748
3073
|
* const registry = createAgentRegistry({ providers: { main: provider } })
|
|
2749
|
-
* const store = createMemoryQueueStore(agentJobShape) // survives a restart
|
|
3074
|
+
* const store = createMemoryQueueStore(agentJobShape) // survives a restart through restore()
|
|
2750
3075
|
* const queue = createAgentQueue({ registry, concurrency: 2, retries: 1, store })
|
|
2751
3076
|
* const result = await queue.enqueue({ provider: 'main', messages: [{ role: 'user', content: 'ok?' }] })
|
|
2752
3077
|
* ```
|
|
2753
3078
|
*/
|
|
2754
3079
|
function createAgentQueue(options) {
|
|
2755
|
-
const { registry,
|
|
3080
|
+
const { registry, partial = false, concurrency, retries, timeout, store } = options;
|
|
2756
3081
|
return createQueue({
|
|
2757
3082
|
...concurrency === void 0 ? {} : { concurrency },
|
|
2758
3083
|
...retries === void 0 ? {} : { retries },
|
|
2759
3084
|
...timeout === void 0 ? {} : { timeout },
|
|
2760
3085
|
...store === void 0 ? {} : { store },
|
|
2761
|
-
handler: handleAgentQueueJob.bind(void 0, registry,
|
|
3086
|
+
handler: handleAgentQueueJob.bind(void 0, registry, partial)
|
|
2762
3087
|
});
|
|
2763
3088
|
}
|
|
2764
3089
|
/**
|
|
2765
|
-
*
|
|
3090
|
+
* Creates an agent-job runner — a {@link RunnerInterface} over serializable
|
|
2766
3091
|
* {@link AgentJobInput}s that COMPOSES `createRunner` (one-shot, ordered, fail-fast), each
|
|
2767
3092
|
* unit rehydrated through the `registry` and subjected to the partial policy. The runner
|
|
2768
3093
|
* enables **sub-agent fan-out**: a parent job's handler can `controller.spawn(childJob)`.
|
|
@@ -2777,17 +3102,17 @@ function createAgentQueue(options) {
|
|
|
2777
3102
|
* runner, FAN OUT and return — do NOT inline-`await` a spawn from within the handler (a
|
|
2778
3103
|
* slot-holding handler awaiting its own spawn can deadlock; see `ControllerInterface`).
|
|
2779
3104
|
* - **Partial policy + cancellation.** Same as `createAgentQueue`: a partial result
|
|
2780
|
-
* THROWS by default (the run's fail-fast engages), `
|
|
3105
|
+
* THROWS by default (the run's fail-fast engages), `partial: true` resolves it; the
|
|
2781
3106
|
* handler threads `controller.signal` into `registry.build`, so a runner abort / a
|
|
2782
3107
|
* per-attempt timeout cancels the agent.
|
|
2783
3108
|
*
|
|
2784
|
-
* @param options - The `registry`, the `
|
|
3109
|
+
* @param options - The `registry`, the `partial` policy, and the substrate knobs
|
|
2785
3110
|
* (`concurrency` / `retries` / `timeout`) (see {@link AgentRunnerOptions})
|
|
2786
3111
|
* @returns A {@link RunnerInterface} of {@link AgentJobInput} → {@link AgentResult}
|
|
2787
3112
|
*
|
|
2788
3113
|
* @example
|
|
2789
3114
|
* ```ts
|
|
2790
|
-
* import { createAgentRunner, createAgentRegistry } from '@
|
|
3115
|
+
* import { createAgentRunner, createAgentRegistry } from '@orkestrel/agent'
|
|
2791
3116
|
*
|
|
2792
3117
|
* const registry = createAgentRegistry({ providers: { main: provider } })
|
|
2793
3118
|
* const runner = createAgentRunner({ registry, concurrency: 2 })
|
|
@@ -2798,15 +3123,15 @@ function createAgentQueue(options) {
|
|
|
2798
3123
|
* ```
|
|
2799
3124
|
*/
|
|
2800
3125
|
function createAgentRunner(options) {
|
|
2801
|
-
const { registry,
|
|
3126
|
+
const { registry, partial = false, concurrency, retries, timeout } = options;
|
|
2802
3127
|
return createRunner({
|
|
2803
3128
|
...concurrency === void 0 ? {} : { concurrency },
|
|
2804
3129
|
...retries === void 0 ? {} : { retries },
|
|
2805
3130
|
...timeout === void 0 ? {} : { timeout },
|
|
2806
|
-
handler: handleAgentRunnerJob.bind(void 0, registry,
|
|
3131
|
+
handler: handleAgentRunnerJob.bind(void 0, registry, partial)
|
|
2807
3132
|
});
|
|
2808
3133
|
}
|
|
2809
3134
|
//#endregion
|
|
2810
|
-
export { Agent, AgentContext, AgentError, AgentJobError, AgentRegistry, Authority, CONVERSATION_RECAP_PREFIX, Channel, Conversation, ConversationError, ConversationManager, DEFAULT_AGENT_LIMIT, DEFAULT_AUTHORITY_ZONE, DEFAULT_CONVERSATION_KEEP, DatabaseConversationStore, IMAGE_TOKEN_ESTIMATE, Instruction, InstructionManager, MESSAGE_TOKEN_OVERHEAD, MemoryConversationStore, ProviderAbortError, Scope, ScopeManager, THINK_CLOSE, THINK_OPEN, ThinkSplitter, WORKSPACE_SECTION_HEADER, agentResultToJSON, createAgent, createAgentContext, createAgentQueue, createAgentRegistry, createAgentRunner, createAuthority, createConversation, createConversationManager, createDatabaseConversationStore, createInstruction, createInstructionManager, createMemoryConversationStore, createScope, createScopeManager, createThinkSplitter, estimateMessages, estimateTokens,
|
|
3135
|
+
export { Agent, AgentContext, AgentError, AgentJobError, AgentRegistry, Authority, CONVERSATION_RECAP_PREFIX, Channel, Conversation, ConversationError, ConversationManager, DEFAULT_AGENT_LIMIT, DEFAULT_AUTHORITY_ZONE, DEFAULT_CONVERSATION_KEEP, DatabaseConversationStore, IMAGE_TOKEN_ESTIMATE, Instruction, InstructionManager, MESSAGE_TOKEN_OVERHEAD, MemoryConversationStore, ProviderAbortError, Scope, ScopeManager, THINK_CLOSE, THINK_OPEN, ThinkSplitter, WORKSPACE_SECTION_HEADER, agentResultToJSON, assembleResult, attachImages, attachUserImages, buildRecapMessage, buildSummaryMessage, collectImageData, createAgent, createAgentContext, createAgentQueue, createAgentRegistry, createAgentRunner, createAuthority, createChannel, createConversation, createConversationManager, createDatabaseConversationStore, createInstruction, createInstructionManager, createMemoryConversationStore, createScope, createScopeManager, createThinkSplitter, denyCall, estimateMessages, estimateTokens, filterAllowList, handleAgentQueueJob, handleAgentRunnerJob, intersectKeys, isAgentError, isAgentJobError, isConversationError, isConversationSnapshot, isMessage, isProviderAbortError, isSection, joinThinking, renderFencedFile, renderSection, resolveClose, resolveItem, resolveOpen, sanitizeToken, sanitizeUsage, settleAgentJob, sumUsage };
|
|
2811
3136
|
|
|
2812
3137
|
//# sourceMappingURL=index.js.map
|