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