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