@orkestrel/agent 0.0.6 → 0.0.8

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.
@@ -73,6 +73,24 @@ var THINK_CLOSE = "</think>";
73
73
  */
74
74
  var WORKSPACE_SECTION_HEADER = "## Workspace";
75
75
  /**
76
+ * The estimated per-message role/framing overhead {@link import('./helpers.js').estimateMessages}
77
+ * adds on top of a message's content estimate — accounts for the fixed wire framing every
78
+ * conversation turn carries (its role tag, delimiters) that {@link import('./helpers.js').estimateTokens}'s
79
+ * content-only heuristic does not otherwise capture.
80
+ */
81
+ var MESSAGE_TOKEN_OVERHEAD = 4;
82
+ /**
83
+ * The coarse, deliberately-approximate per-image token cost {@link import('./helpers.js').estimateMessages}
84
+ * charges for each attached image.
85
+ *
86
+ * @remarks
87
+ * A base64 image payload's LENGTH is NOT a reliable token proxy (a vision model's actual image
88
+ * token cost depends on resolution / tiling, not byte size), so this is a fixed, coarse
89
+ * per-image estimate rather than a derivation from `image.length` — a planning heuristic, not an
90
+ * exact count.
91
+ */
92
+ var IMAGE_TOKEN_ESTIMATE = 512;
93
+ /**
76
94
  * The extension→language table {@link import('./helpers.js').inferLanguage} reads to
77
95
  * map a file path's extension to a fenced-code language tag. Covers the common source /
78
96
  * markup / data extensions; an unlisted extension falls back to `'text'`.
@@ -201,18 +219,21 @@ function isAgentJobError(value) {
201
219
  }
202
220
  /**
203
221
  * Thrown by a {@link ConversationInterface}'s `compact()` when the conversation has no
204
- * {@link ConversationSummarizer} to fold its messages with — carries a machine-readable
205
- * `code`.
222
+ * {@link ConversationSummarizer} to fold its messages with, or when its `sections` cap is
223
+ * structurally invalid — carries a machine-readable `code`.
206
224
  *
207
225
  * @remarks
208
226
  * Compaction REQUIRES a summarizer (it digests the folded slice into a section summary and
209
227
  * regenerates the rollup); a conversation created without one can still store + `view()` its
210
- * live tail, but a `compact()` is a programmer error (§12) and throws this. `code` is
211
- * `'SUMMARIZER'` (the only condition so far). Narrow a caught value with
228
+ * live tail, but a `compact()` is a programmer error (§12) and throws this with `'SUMMARIZER'`.
229
+ * A `sections` cap (§F2, on {@link import('./types.js').ConversationOptions} /
230
+ * {@link import('./types.js').ConversationManagerOptions} /
231
+ * {@link import('./types.js').CompactOptions}) must be `>= 1` — a sub-1 cap is a programmer
232
+ * error and throws this with `'SECTIONS'`. Narrow a caught value with
212
233
  * {@link isConversationError} and branch on `error.code`.
213
234
  */
214
235
  var ConversationError = class extends Error {
215
- /** The machine-readable condition — `'SUMMARIZER'`: a `compact()` with no summarizer. */
236
+ /** The machine-readable condition — `'SUMMARIZER'`: a `compact()` with no summarizer; `'SECTIONS'`: a sub-1 `sections` cap. */
216
237
  code;
217
238
  constructor(code, message) {
218
239
  super(message);
@@ -277,6 +298,45 @@ var WorkspaceError = class extends Error {
277
298
  function isWorkspaceError(value) {
278
299
  return value instanceof WorkspaceError;
279
300
  }
301
+ /**
302
+ * Thrown synchronously by an {@link AgentInterface}'s `stream()` when a concurrent run would
303
+ * corrupt SHARED per-agent accounting — carries a machine-readable `code`.
304
+ *
305
+ * @remarks
306
+ * A run already in flight on the same agent, PLUS a construction-level `window` (a shared
307
+ * context budget) or a construction-level `budget` with no per-run override (a shared cost
308
+ * budget), means a second concurrent `stream()` would race its charges against the same
309
+ * shared instance — corrupting the accounting. `code` is `'CONCURRENCY'` (the only condition
310
+ * so far). Use separate agents, or per-run `budget` overrides with no `window`, for genuinely
311
+ * concurrent runs. Narrow a caught value with {@link isAgentError} and branch on `error.code`.
312
+ */
313
+ var AgentError = class extends Error {
314
+ /** The machine-readable condition — `'CONCURRENCY'`: a concurrent run on a shared accounting agent. */
315
+ code;
316
+ constructor(code, message) {
317
+ super(message);
318
+ this.name = "AgentError";
319
+ this.code = code;
320
+ }
321
+ };
322
+ /**
323
+ * Narrow an unknown caught value to an {@link AgentError}.
324
+ *
325
+ * @param value - The value to test (typically a `catch` binding)
326
+ * @returns `true` when `value` is an {@link AgentError}
327
+ *
328
+ * @example
329
+ * ```ts
330
+ * try {
331
+ * agent.stream()
332
+ * } catch (error) {
333
+ * if (isAgentError(error) && error.code === 'CONCURRENCY') useSeparateAgents()
334
+ * }
335
+ * ```
336
+ */
337
+ function isAgentError(value) {
338
+ return value instanceof AgentError;
339
+ }
280
340
  //#endregion
281
341
  //#region src/core/conversations/Conversation.ts
282
342
  /**
@@ -330,6 +390,7 @@ var Conversation = class {
330
390
  #emitter;
331
391
  #summarize;
332
392
  #keep;
393
+ #cap;
333
394
  #sections = [];
334
395
  #summary;
335
396
  #messages = /* @__PURE__ */ new Map();
@@ -341,6 +402,8 @@ var Conversation = class {
341
402
  });
342
403
  this.#summarize = options?.summarize;
343
404
  this.#keep = options?.keep ?? 0;
405
+ if (options?.sections !== void 0 && options.sections < 1) throw new ConversationError("SECTIONS", "a sections cap must be >= 1");
406
+ this.#cap = options?.sections;
344
407
  if (seed) {
345
408
  this.#summary = seed.summary;
346
409
  for (const section of seed.sections) this.#sections.push(section);
@@ -392,6 +455,8 @@ var Conversation = class {
392
455
  async compact(options) {
393
456
  const summarize = this.#summarize;
394
457
  if (summarize === void 0) throw new ConversationError("SUMMARIZER", "cannot compact a conversation without a summarizer");
458
+ const cap = options?.sections ?? this.#cap;
459
+ if (cap !== void 0 && cap < 1) throw new ConversationError("SECTIONS", "a sections cap must be >= 1");
395
460
  const keep = options?.keep ?? this.#keep;
396
461
  const live = [...this.#messages.values()];
397
462
  const fold = keep <= 0 ? live.length : live.length - keep;
@@ -405,6 +470,23 @@ var Conversation = class {
405
470
  };
406
471
  for (const message of slice) this.#messages.delete(message.id);
407
472
  this.#sections.push(section);
473
+ if (cap !== void 0 && this.#sections.length > cap) {
474
+ const overflow = this.#sections.length - cap + 1;
475
+ const folded = this.#sections.slice(0, overflow);
476
+ try {
477
+ const merged = {
478
+ id: crypto.randomUUID(),
479
+ summary: await summarize(folded.map((one) => this.#summaryMessage(one))),
480
+ messages: folded.flatMap((one) => one.messages)
481
+ };
482
+ this.#sections.splice(0, overflow, merged);
483
+ this.#emitter.emit("collapse", merged);
484
+ } catch (error) {
485
+ this.#summary = await summarize(this.#sections.map((one) => this.#summaryMessage(one)));
486
+ this.#emitter.emit("summary", this.#summary);
487
+ throw error;
488
+ }
489
+ }
408
490
  this.#summary = await summarize(this.#sections.map((one) => this.#summaryMessage(one)));
409
491
  this.#emitter.emit("summary", this.#summary);
410
492
  this.#emitter.emit("compact", section);
@@ -504,10 +586,12 @@ var ConversationManager = class {
504
586
  #active;
505
587
  #summarize;
506
588
  #keep;
589
+ #sections;
507
590
  #store;
508
591
  constructor(options) {
509
592
  this.#summarize = options?.summarize;
510
593
  this.#keep = options?.keep ?? 0;
594
+ this.#sections = options?.sections;
511
595
  this.#store = options?.store;
512
596
  }
513
597
  get count() {
@@ -523,11 +607,13 @@ var ConversationManager = class {
523
607
  return [...this.#conversations.values()];
524
608
  }
525
609
  add(input) {
610
+ const sections = input?.sections ?? this.#sections;
526
611
  const conversation = new Conversation({
527
612
  ...input?.id === void 0 ? {} : { id: input.id },
528
613
  ...input?.on === void 0 ? {} : { on: input.on },
529
614
  summarize: input?.summarize ?? this.#summarize,
530
- keep: input?.keep ?? this.#keep
615
+ keep: input?.keep ?? this.#keep,
616
+ ...sections === void 0 ? {} : { sections }
531
617
  }, input?.snapshot);
532
618
  this.#conversations.set(conversation.id, conversation);
533
619
  if (this.#active === void 0) this.#active = conversation.id;
@@ -668,23 +754,40 @@ function estimateTokens(text) {
668
754
  * (the {@link import('./types.js').AgentOptions} `window`).
669
755
  *
670
756
  * @remarks
671
- * Sums {@link estimateTokens} over each message's `content` (the `ceil(length / 4)` char
672
- * heuristic), so it is deterministic and provider-free — the same messages always yield the
673
- * same estimate, with an empty batch `0`. It is the fully-swappable default an agent's
674
- * auto-compaction context budget charges each turn's new messages through; a caller wanting a
675
- * sharper count supplies its own `consume` to `createBudget` instead. Total — never throws.
757
+ * Sums, per message, {@link estimateTokens} over its `content` (the `ceil(length / 4)` char
758
+ * heuristic) PLUS {@link import('./constants.js').MESSAGE_TOKEN_OVERHEAD} (a fixed per-message
759
+ * role/framing overhead) PLUS, when present, {@link estimateTokens} over its JSON-stringified
760
+ * `calls` PLUS `images.length * `{@link import('./constants.js').IMAGE_TOKEN_ESTIMATE} (a coarse,
761
+ * deliberately-approximate per-image cost — a base64 length is NOT a token proxy). Deterministic
762
+ * and provider-free — the same messages always yield the same estimate, with an empty batch `0`.
763
+ * It is the fully-swappable default an agent's auto-compaction context budget charges each
764
+ * turn's new messages through; a caller wanting a sharper count supplies its own `consume` to
765
+ * `createBudget` instead. Total — never throws: a `calls` `JSON.stringify` that throws (a
766
+ * circular `ToolCall.arguments`) is caught and replaced with a conservative fixed contribution of
767
+ * {@link import('./constants.js').MESSAGE_TOKEN_OVERHEAD} (the same per-message overhead scale)
768
+ * instead of estimating the (unreachable) serialized length.
676
769
  *
677
770
  * @param messages - The messages to estimate (a turn's appended assistant + tool messages)
678
- * @returns The summed estimated token count (`Σ estimateTokens(m.content)`; `0` when empty)
771
+ * @returns The summed estimated token count (`0` when empty)
679
772
  *
680
773
  * @example
681
774
  * ```ts
682
775
  * estimateMessages([]) // 0
683
- * estimateMessages([{ id: '1', role: 'user', content: 'hello' }]) // 2
776
+ * estimateMessages([{ id: '1', role: 'user', content: 'hello' }]) // 6 (2 content + 4 overhead)
684
777
  * ```
685
778
  */
686
779
  function estimateMessages(messages) {
687
- return messages.reduce((sum, message) => sum + estimateTokens(message.content), 0);
780
+ return messages.reduce((sum, message) => {
781
+ const content = estimateTokens(message.content) + 4;
782
+ let calls = 0;
783
+ if (message.calls?.length) try {
784
+ calls = estimateTokens(JSON.stringify(message.calls));
785
+ } catch {
786
+ calls = 4;
787
+ }
788
+ const images = (message.images?.length ?? 0) * 512;
789
+ return sum + content + calls + images;
790
+ }, 0);
688
791
  }
689
792
  /**
690
793
  * Run one rehydrated agent and apply the partial-as-configurable-failure policy — the
@@ -1310,6 +1413,32 @@ function fencedFile(path, language, content) {
1310
1413
  function escapeRegExp(value) {
1311
1414
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1312
1415
  }
1416
+ /**
1417
+ * Sanitize a {@link TokenUsage} into safe, non-negative integers — the guard an agent's
1418
+ * abort-usage path applies to a provider's partial usage before it is charged against a
1419
+ * budget or folded into the run total.
1420
+ *
1421
+ * @remarks
1422
+ * Per field (`prompt` / `completion` / `total`): a non-finite value (`NaN`, `+Infinity`,
1423
+ * `-Infinity`) or a negative value floors to `0`; a fractional value floors to its
1424
+ * non-negative integer part. No upper cap is applied. Total — never throws.
1425
+ *
1426
+ * @param usage - The token usage to sanitize (e.g. a provider's abort-partial usage)
1427
+ * @returns A new {@link TokenUsage} with every field a safe non-negative integer
1428
+ *
1429
+ * @example
1430
+ * ```ts
1431
+ * sanitizeUsage({ prompt: -5, completion: NaN, total: 12.7 }) // { prompt: 0, completion: 0, total: 12 }
1432
+ * ```
1433
+ */
1434
+ function sanitizeUsage(usage) {
1435
+ const clean = (value) => Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
1436
+ return {
1437
+ prompt: clean(usage.prompt),
1438
+ completion: clean(usage.completion),
1439
+ total: clean(usage.total)
1440
+ };
1441
+ }
1313
1442
  //#endregion
1314
1443
  //#region src/core/instructions/Instruction.ts
1315
1444
  /**
@@ -2235,6 +2364,7 @@ var Agent = class {
2235
2364
  #signal;
2236
2365
  #authority;
2237
2366
  #window;
2367
+ #strict;
2238
2368
  #emitter;
2239
2369
  id = crypto.randomUUID();
2240
2370
  #status = "idle";
@@ -2244,6 +2374,9 @@ var Agent = class {
2244
2374
  this.#context = new AgentContext({
2245
2375
  system: options?.system,
2246
2376
  tools: options?.tools,
2377
+ instructions: options?.instructions,
2378
+ workspaces: options?.workspaces,
2379
+ scope: options?.scope,
2247
2380
  conversations: options?.conversations
2248
2381
  });
2249
2382
  this.#limit = options?.limit ?? 10;
@@ -2253,6 +2386,7 @@ var Agent = class {
2253
2386
  this.#signal = options?.signal;
2254
2387
  this.#authority = options?.authority;
2255
2388
  this.#window = options?.window;
2389
+ this.#strict = options?.strict ?? false;
2256
2390
  this.#emitter = new Emitter({
2257
2391
  on: options?.on,
2258
2392
  error: options?.error
@@ -2271,6 +2405,7 @@ var Agent = class {
2271
2405
  return this.stream(options).result;
2272
2406
  }
2273
2407
  stream(options) {
2408
+ if (this.#runs.size > 0 && (this.#window !== void 0 || this.#budget !== void 0 && options?.budget === void 0)) throw new AgentError("CONCURRENCY", "concurrent runs on one agent with a shared construction window/budget corrupt accounting; use separate agents or per-run budgets");
2274
2409
  const timeoutMs = options?.timeout ?? this.#timeoutMs;
2275
2410
  const timeout = timeoutMs === void 0 ? void 0 : createTimeout({ ms: timeoutMs });
2276
2411
  timeout?.start();
@@ -2390,7 +2525,7 @@ var Agent = class {
2390
2525
  if (isProviderAbortError(error)) {
2391
2526
  if (error.partial.thinking !== void 0) thinking = this.#thought(thinking, error.partial.thinking);
2392
2527
  if (error.partial.usage !== void 0) {
2393
- const abortUsage = error.partial.usage;
2528
+ const abortUsage = sanitizeUsage(error.partial.usage);
2394
2529
  budget?.consume({
2395
2530
  prompt: abortUsage.prompt,
2396
2531
  completion: Math.max(0, abortUsage.completion - charged),
@@ -2407,16 +2542,17 @@ var Agent = class {
2407
2542
  }
2408
2543
  if (result.thinking !== void 0 && result.thinking.length > 0) thinking = this.#thought(thinking, result.thinking);
2409
2544
  if (result.usage !== void 0) {
2545
+ const resultUsage = sanitizeUsage(result.usage);
2410
2546
  budget?.consume({
2411
- prompt: result.usage.prompt,
2412
- completion: Math.max(0, result.usage.completion - charged),
2413
- total: Math.max(0, result.usage.total - charged)
2547
+ prompt: resultUsage.prompt,
2548
+ completion: Math.max(0, resultUsage.completion - charged),
2549
+ total: Math.max(0, resultUsage.total - charged)
2414
2550
  });
2415
- usage = this.#sum(usage, result.usage);
2416
- this.#emitter.emit("usage", result.usage);
2551
+ usage = this.#sum(usage, resultUsage);
2552
+ this.#emitter.emit("usage", resultUsage);
2417
2553
  yield {
2418
2554
  type: "usage",
2419
- usage: result.usage
2555
+ usage: resultUsage
2420
2556
  };
2421
2557
  }
2422
2558
  if (result.tools !== void 0 && result.tools.length > 0) {
@@ -2475,6 +2611,7 @@ var Agent = class {
2475
2611
  section = await conversation.compact();
2476
2612
  } catch (error) {
2477
2613
  this.#emitter.emit("compactError", error);
2614
+ if (this.#strict) throw error;
2478
2615
  return;
2479
2616
  }
2480
2617
  if (section === void 0) {
@@ -4061,6 +4198,6 @@ function createWorkspaceManager(options) {
4061
4198
  return new WorkspaceManager(options);
4062
4199
  }
4063
4200
  //#endregion
4064
- export { Agent, AgentContext, AgentJobError, AgentRegistry, Authority, CONVERSATION_RECAP_PREFIX, Channel, Conversation, ConversationError, ConversationManager, DEFAULT_AGENT_LIMIT, DEFAULT_AUTHORITY_ZONE, DEFAULT_CONVERSATION_KEEP, DatabaseConversationStore, DatabaseWorkspaceStore, EXTENSION_TO_LANGUAGE, Instruction, InstructionManager, MemoryConversationStore, MemoryWorkspaceStore, ProviderAbortError, Scope, ScopeManager, THINK_CLOSE, THINK_OPEN, ThinkSplitter, Tool, ToolManager, WORKSPACE_SECTION_HEADER, Workspace, WorkspaceError, WorkspaceManager, buildToolResult, clampPosition, clampRange, computeSize, countLines, createAgent, createAgentContext, createAgentQueue, createAgentRegistry, createAgentRunner, createAuthority, createBinaryContent, createConversation, createConversationManager, createDatabaseConversationStore, createDatabaseWorkspaceStore, createFile, createInstruction, createInstructionManager, createMemoryConversationStore, createMemoryWorkspaceStore, createScope, createScopeManager, createTextContent, createThinkSplitter, createTool, createToolManager, createWorkspace, createWorkspaceManager, decodedSize, escapeRegExp, estimateMessages, estimateTokens, fencedFile, filterAllowList, inferLanguage, isAgentJobError, isBinary, isConversationError, isConversationSnapshot, isFile, isImage, isMessage, isProviderAbortError, isSection, isText, isToolCall, isValidRange, isWorkspaceError, isWorkspaceSnapshot, offsetAt, rangeOf, settleAgentJob, sliceRange, spliceRange };
4201
+ export { Agent, AgentContext, AgentError, AgentJobError, AgentRegistry, Authority, CONVERSATION_RECAP_PREFIX, Channel, Conversation, ConversationError, ConversationManager, DEFAULT_AGENT_LIMIT, DEFAULT_AUTHORITY_ZONE, DEFAULT_CONVERSATION_KEEP, DatabaseConversationStore, DatabaseWorkspaceStore, EXTENSION_TO_LANGUAGE, IMAGE_TOKEN_ESTIMATE, Instruction, InstructionManager, MESSAGE_TOKEN_OVERHEAD, MemoryConversationStore, MemoryWorkspaceStore, ProviderAbortError, Scope, ScopeManager, THINK_CLOSE, THINK_OPEN, ThinkSplitter, Tool, ToolManager, WORKSPACE_SECTION_HEADER, Workspace, WorkspaceError, WorkspaceManager, buildToolResult, clampPosition, clampRange, computeSize, countLines, createAgent, createAgentContext, createAgentQueue, createAgentRegistry, createAgentRunner, createAuthority, createBinaryContent, createConversation, createConversationManager, createDatabaseConversationStore, createDatabaseWorkspaceStore, createFile, createInstruction, createInstructionManager, createMemoryConversationStore, createMemoryWorkspaceStore, createScope, createScopeManager, createTextContent, createThinkSplitter, createTool, createToolManager, createWorkspace, createWorkspaceManager, decodedSize, escapeRegExp, estimateMessages, estimateTokens, fencedFile, filterAllowList, inferLanguage, isAgentError, isAgentJobError, isBinary, isConversationError, isConversationSnapshot, isFile, isImage, isMessage, isProviderAbortError, isSection, isText, isToolCall, isValidRange, isWorkspaceError, isWorkspaceSnapshot, offsetAt, rangeOf, sanitizeUsage, settleAgentJob, sliceRange, spliceRange };
4065
4202
 
4066
4203
  //# sourceMappingURL=index.js.map