@orkestrel/agent 0.0.4 → 0.0.6

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.
@@ -378,6 +378,13 @@ export declare type AgentEventMap = {
378
378
  readonly error: readonly [error: unknown];
379
379
  /** The run was cancelled (external signal / timeout / budget / `abort()`) — the cancel reason. */
380
380
  readonly abort: readonly [reason: unknown];
381
+ /**
382
+ * The loop exhausted its `limit` while still holding unresolved tool intent (the model
383
+ * requested tools on the very last allowed turn) — the turn count reached. Distinct from
384
+ * `abort`: exhaustion is NOT a cancel (no external signal / timeout / budget tripped), so
385
+ * this fires INSTEAD of `abort`, still followed by `finish` carrying the partial result.
386
+ */
387
+ readonly exhaust: readonly [turns: number];
381
388
  /**
382
389
  * AUTOMATIC compaction's summarizer THREW — a NON-FATAL warn channel (the run continues; see
383
390
  * {@link AgentOptions.window}). When the loop's between-turns / pre-first-turn auto-compaction
@@ -735,9 +742,17 @@ export declare interface AgentRegistryOptions {
735
742
  * `partial` is `true` when the turn was committed early from a cancel — an external
736
743
  * `signal` abort, the turn's own `abort()`, a `timeout` deadline, or an exhausted
737
744
  * `budget` — in which case `content` is whatever had accumulated when the cancel
738
- * landed. It is `false` for a turn that ran to a natural finish. `usage` is present
739
- * only when at least one provider call reported usage. `thinking` is present only
740
- * when a provider call surfaced reasoning it separated from the answer
745
+ * landed. `partial` is ALSO `true` when the loop exhausted its `limit` while still
746
+ * holding unresolved tool intent (the model requested tools on the very last allowed
747
+ * turn) — a distinct, non-cancel cause covered by {@link RunOutcome.exhausted} (see
748
+ * the `exhaust` {@link AgentEventMap} event). It is `false` for a turn that ran to a
749
+ * natural finish (including a `limit: 0` run, which never enters the loop). `usage` is
750
+ * present only when at least one provider call reported usage — an aborted run's `usage`
751
+ * INCLUDES the cancelled turn's tokens when the provider reports partial usage on the
752
+ * abort (folded in exactly like a completed turn's); a provider that cannot observe
753
+ * usage mid-stream (e.g. a daemon whose final counts never arrive before the cancel)
754
+ * reports none for that turn, and none is fabricated. `thinking` is present
755
+ * only when a provider call surfaced reasoning it separated from the answer
741
756
  * ({@link ProviderResult.thinking}, joined across the run's calls) — display/audit
742
757
  * metadata that never re-enters the conversation.
743
758
  */
@@ -802,6 +817,34 @@ export declare interface AgentRunOptions {
802
817
  * behaviour), so a caller that passes no options runs exactly as before.
803
818
  */
804
819
  readonly think?: boolean;
820
+ /**
821
+ * Constrain the response to this JSON-Schema shape, forwarded to the provider's `stream`
822
+ * as {@link ProviderStreamOptions.schema} — a per-run structured-output request. Omitted ⇒
823
+ * no constraint (the loop is byte-for-byte the prior behaviour).
824
+ */
825
+ readonly schema?: Readonly<Record<string, unknown>>;
826
+ /**
827
+ * Overrides {@link AgentOptions.limit} for THIS run only — the max tool-iteration turns
828
+ * before the loop stops. Omitted ⇒ the agent's constructed `limit` applies.
829
+ */
830
+ readonly limit?: number;
831
+ /**
832
+ * Overrides {@link AgentOptions.timeout} for THIS run only — a wall-clock deadline (ms)
833
+ * whose abort commits a partial result. Omitted ⇒ the agent's constructed `timeout` applies.
834
+ */
835
+ readonly timeout?: number;
836
+ /**
837
+ * Overrides {@link AgentOptions.budget} for THIS run only — a token cost bound whose abort
838
+ * commits a partial result; `start()`ed for this run exactly as the constructed budget is.
839
+ * Omitted ⇒ the agent's constructed `budget` applies.
840
+ */
841
+ readonly budget?: BudgetInterface<TokenUsage>;
842
+ /**
843
+ * An additional per-run external cancel, COMPOSED with {@link AgentOptions.signal} (both
844
+ * fold into the run's bound abort via `AbortSignal.any` — neither is dropped). Omitted ⇒
845
+ * only the agent's constructed `signal` (if any) applies.
846
+ */
847
+ readonly signal?: AbortSignal;
805
848
  }
806
849
 
807
850
  /**
@@ -3791,13 +3834,18 @@ export declare interface ProviderResult {
3791
3834
  * `think` OVERRIDES the provider's constructed reasoning preference for THIS call: `true`
3792
3835
  * asks the backend to separate reasoning natively (a thinking model returns it on its
3793
3836
  * `message.thinking` channel, surfaced as `'thinking'` {@link ProviderDelta}s + the final
3794
- * {@link ProviderResult.thinking}); `false` suppresses it. Omitted ⇒ the provider's own
3795
- * default applies (the constructor value), so the contract stays backward-safe — a caller
3837
+ * {@link ProviderResult.thinking}); `false` suppresses it. `schema`, when given, asks the
3838
+ * backend to constrain its response to the given JSON-Schema shape (the same open
3839
+ * JSON-Schema record {@link ToolDefinition.parameters} already carries) — a structured-output
3840
+ * request for THIS call only. Both omitted ⇒ the provider's own defaults apply (the
3841
+ * constructor value / no schema constraint), so the contract stays backward-safe — a caller
3796
3842
  * that passes no options behaves exactly as before.
3797
3843
  */
3798
3844
  export declare interface ProviderStreamOptions {
3799
3845
  /** Override the provider's reasoning preference for this call; omitted ⇒ the provider default. */
3800
3846
  readonly think?: boolean;
3847
+ /** Constrain the response to this JSON-Schema shape (the same open record {@link ToolDefinition.parameters} uses); omitted ⇒ no constraint. */
3848
+ readonly schema?: Readonly<Record<string, unknown>>;
3801
3849
  }
3802
3850
 
3803
3851
  /**
@@ -3891,16 +3939,19 @@ export declare interface ReplaceResult {
3891
3939
  * the loop: `content` accumulates the streamed assistant text, `thinking` the
3892
3940
  * reasoning the provider calls separated from it ({@link ProviderResult.thinking},
3893
3941
  * joined across calls — `undefined` until one surfaces it), `usage` the summed
3894
- * {@link TokenUsage} (present only when a provider call reported it), and `partial`
3895
- * flips `true` when a cancel commits the run early. It is the INTERNAL precursor to
3896
- * the settled `AgentResult` — the loop reads it back to assemble the public result —
3897
- * not a caller-facing shape.
3942
+ * {@link TokenUsage} (present only when a provider call reported it), `partial`
3943
+ * flips `true` when a cancel commits the run early OR when the loop exhausts its
3944
+ * `limit` with unresolved tool intent, and `exhausted` flips `true` in that second
3945
+ * case specifically (a distinct, non-cancel cause the {@link AgentEventMap} `exhaust`
3946
+ * event observes). It is the INTERNAL precursor to the settled `AgentResult` — the
3947
+ * loop reads it back to assemble the public result — not a caller-facing shape.
3898
3948
  */
3899
3949
  export declare interface RunOutcome {
3900
3950
  content: string;
3901
3951
  thinking: string | undefined;
3902
3952
  usage: TokenUsage | undefined;
3903
3953
  partial: boolean;
3954
+ exhausted: boolean;
3904
3955
  }
3905
3956
 
3906
3957
  /**
@@ -378,6 +378,13 @@ export declare type AgentEventMap = {
378
378
  readonly error: readonly [error: unknown];
379
379
  /** The run was cancelled (external signal / timeout / budget / `abort()`) — the cancel reason. */
380
380
  readonly abort: readonly [reason: unknown];
381
+ /**
382
+ * The loop exhausted its `limit` while still holding unresolved tool intent (the model
383
+ * requested tools on the very last allowed turn) — the turn count reached. Distinct from
384
+ * `abort`: exhaustion is NOT a cancel (no external signal / timeout / budget tripped), so
385
+ * this fires INSTEAD of `abort`, still followed by `finish` carrying the partial result.
386
+ */
387
+ readonly exhaust: readonly [turns: number];
381
388
  /**
382
389
  * AUTOMATIC compaction's summarizer THREW — a NON-FATAL warn channel (the run continues; see
383
390
  * {@link AgentOptions.window}). When the loop's between-turns / pre-first-turn auto-compaction
@@ -735,9 +742,17 @@ export declare interface AgentRegistryOptions {
735
742
  * `partial` is `true` when the turn was committed early from a cancel — an external
736
743
  * `signal` abort, the turn's own `abort()`, a `timeout` deadline, or an exhausted
737
744
  * `budget` — in which case `content` is whatever had accumulated when the cancel
738
- * landed. It is `false` for a turn that ran to a natural finish. `usage` is present
739
- * only when at least one provider call reported usage. `thinking` is present only
740
- * when a provider call surfaced reasoning it separated from the answer
745
+ * landed. `partial` is ALSO `true` when the loop exhausted its `limit` while still
746
+ * holding unresolved tool intent (the model requested tools on the very last allowed
747
+ * turn) — a distinct, non-cancel cause covered by {@link RunOutcome.exhausted} (see
748
+ * the `exhaust` {@link AgentEventMap} event). It is `false` for a turn that ran to a
749
+ * natural finish (including a `limit: 0` run, which never enters the loop). `usage` is
750
+ * present only when at least one provider call reported usage — an aborted run's `usage`
751
+ * INCLUDES the cancelled turn's tokens when the provider reports partial usage on the
752
+ * abort (folded in exactly like a completed turn's); a provider that cannot observe
753
+ * usage mid-stream (e.g. a daemon whose final counts never arrive before the cancel)
754
+ * reports none for that turn, and none is fabricated. `thinking` is present
755
+ * only when a provider call surfaced reasoning it separated from the answer
741
756
  * ({@link ProviderResult.thinking}, joined across the run's calls) — display/audit
742
757
  * metadata that never re-enters the conversation.
743
758
  */
@@ -802,6 +817,34 @@ export declare interface AgentRunOptions {
802
817
  * behaviour), so a caller that passes no options runs exactly as before.
803
818
  */
804
819
  readonly think?: boolean;
820
+ /**
821
+ * Constrain the response to this JSON-Schema shape, forwarded to the provider's `stream`
822
+ * as {@link ProviderStreamOptions.schema} — a per-run structured-output request. Omitted ⇒
823
+ * no constraint (the loop is byte-for-byte the prior behaviour).
824
+ */
825
+ readonly schema?: Readonly<Record<string, unknown>>;
826
+ /**
827
+ * Overrides {@link AgentOptions.limit} for THIS run only — the max tool-iteration turns
828
+ * before the loop stops. Omitted ⇒ the agent's constructed `limit` applies.
829
+ */
830
+ readonly limit?: number;
831
+ /**
832
+ * Overrides {@link AgentOptions.timeout} for THIS run only — a wall-clock deadline (ms)
833
+ * whose abort commits a partial result. Omitted ⇒ the agent's constructed `timeout` applies.
834
+ */
835
+ readonly timeout?: number;
836
+ /**
837
+ * Overrides {@link AgentOptions.budget} for THIS run only — a token cost bound whose abort
838
+ * commits a partial result; `start()`ed for this run exactly as the constructed budget is.
839
+ * Omitted ⇒ the agent's constructed `budget` applies.
840
+ */
841
+ readonly budget?: BudgetInterface<TokenUsage>;
842
+ /**
843
+ * An additional per-run external cancel, COMPOSED with {@link AgentOptions.signal} (both
844
+ * fold into the run's bound abort via `AbortSignal.any` — neither is dropped). Omitted ⇒
845
+ * only the agent's constructed `signal` (if any) applies.
846
+ */
847
+ readonly signal?: AbortSignal;
805
848
  }
806
849
 
807
850
  /**
@@ -3791,13 +3834,18 @@ export declare interface ProviderResult {
3791
3834
  * `think` OVERRIDES the provider's constructed reasoning preference for THIS call: `true`
3792
3835
  * asks the backend to separate reasoning natively (a thinking model returns it on its
3793
3836
  * `message.thinking` channel, surfaced as `'thinking'` {@link ProviderDelta}s + the final
3794
- * {@link ProviderResult.thinking}); `false` suppresses it. Omitted ⇒ the provider's own
3795
- * default applies (the constructor value), so the contract stays backward-safe — a caller
3837
+ * {@link ProviderResult.thinking}); `false` suppresses it. `schema`, when given, asks the
3838
+ * backend to constrain its response to the given JSON-Schema shape (the same open
3839
+ * JSON-Schema record {@link ToolDefinition.parameters} already carries) — a structured-output
3840
+ * request for THIS call only. Both omitted ⇒ the provider's own defaults apply (the
3841
+ * constructor value / no schema constraint), so the contract stays backward-safe — a caller
3796
3842
  * that passes no options behaves exactly as before.
3797
3843
  */
3798
3844
  export declare interface ProviderStreamOptions {
3799
3845
  /** Override the provider's reasoning preference for this call; omitted ⇒ the provider default. */
3800
3846
  readonly think?: boolean;
3847
+ /** Constrain the response to this JSON-Schema shape (the same open record {@link ToolDefinition.parameters} uses); omitted ⇒ no constraint. */
3848
+ readonly schema?: Readonly<Record<string, unknown>>;
3801
3849
  }
3802
3850
 
3803
3851
  /**
@@ -3891,16 +3939,19 @@ export declare interface ReplaceResult {
3891
3939
  * the loop: `content` accumulates the streamed assistant text, `thinking` the
3892
3940
  * reasoning the provider calls separated from it ({@link ProviderResult.thinking},
3893
3941
  * joined across calls — `undefined` until one surfaces it), `usage` the summed
3894
- * {@link TokenUsage} (present only when a provider call reported it), and `partial`
3895
- * flips `true` when a cancel commits the run early. It is the INTERNAL precursor to
3896
- * the settled `AgentResult` — the loop reads it back to assemble the public result —
3897
- * not a caller-facing shape.
3942
+ * {@link TokenUsage} (present only when a provider call reported it), `partial`
3943
+ * flips `true` when a cancel commits the run early OR when the loop exhausts its
3944
+ * `limit` with unresolved tool intent, and `exhausted` flips `true` in that second
3945
+ * case specifically (a distinct, non-cancel cause the {@link AgentEventMap} `exhaust`
3946
+ * event observes). It is the INTERNAL precursor to the settled `AgentResult` — the
3947
+ * loop reads it back to assemble the public result — not a caller-facing shape.
3898
3948
  */
3899
3949
  export declare interface RunOutcome {
3900
3950
  content: string;
3901
3951
  thinking: string | undefined;
3902
3952
  usage: TokenUsage | undefined;
3903
3953
  partial: boolean;
3954
+ exhausted: boolean;
3904
3955
  }
3905
3956
 
3906
3957
  /**
@@ -2271,10 +2271,13 @@ var Agent = class {
2271
2271
  return this.stream(options).result;
2272
2272
  }
2273
2273
  stream(options) {
2274
- const timeout = this.#timeoutMs === void 0 ? void 0 : createTimeout({ ms: this.#timeoutMs });
2274
+ const timeoutMs = options?.timeout ?? this.#timeoutMs;
2275
+ const timeout = timeoutMs === void 0 ? void 0 : createTimeout({ ms: timeoutMs });
2275
2276
  timeout?.start();
2276
- this.#budget?.start();
2277
- const abort = createAbort({ signal: this.#parents(timeout) });
2277
+ const budget = options?.budget ?? this.#budget;
2278
+ budget?.start();
2279
+ const limit = options?.limit ?? this.#limit;
2280
+ const abort = createAbort({ signal: this.#parents(timeout, budget, options?.signal) });
2278
2281
  this.#runs.add(abort);
2279
2282
  this.#status = "running";
2280
2283
  this.#emitter.emit("start", this.id);
@@ -2282,11 +2285,12 @@ var Agent = class {
2282
2285
  content: "",
2283
2286
  thinking: void 0,
2284
2287
  usage: void 0,
2285
- partial: false
2288
+ partial: false,
2289
+ exhausted: false
2286
2290
  };
2287
2291
  const channel = new Channel();
2288
2292
  const settled = createDeferred();
2289
- this.#pump(abort, outcome, timeout, channel, settled, options?.think);
2293
+ this.#pump(abort, outcome, timeout, channel, settled, options?.think, options?.schema, limit, budget);
2290
2294
  settled.promise.catch(() => {});
2291
2295
  return {
2292
2296
  events: this.#events(channel, abort),
@@ -2299,10 +2303,10 @@ var Agent = class {
2299
2303
  abort(reason) {
2300
2304
  for (const abort of [...this.#runs]) abort.abort(reason);
2301
2305
  }
2302
- async #pump(abort, outcome, timeout, channel, settled, think) {
2306
+ async #pump(abort, outcome, timeout, channel, settled, think, schema, limit, budget) {
2303
2307
  let failure;
2304
2308
  try {
2305
- for await (const chunk of this.#run(abort, outcome, think)) channel.push(chunk);
2309
+ for await (const chunk of this.#run(abort, outcome, think, schema, limit, budget)) channel.push(chunk);
2306
2310
  } catch (error) {
2307
2311
  failure = { error };
2308
2312
  } finally {
@@ -2313,7 +2317,8 @@ var Agent = class {
2313
2317
  channel.close();
2314
2318
  const result = this.#result(outcome);
2315
2319
  settled.resolve(result);
2316
- if (outcome.partial) this.#emitter.emit("abort", abort.signal.reason);
2320
+ if (outcome.exhausted) this.#emitter.emit("exhaust", limit);
2321
+ else if (outcome.partial) this.#emitter.emit("abort", abort.signal.reason);
2317
2322
  this.#emitter.emit("finish", result);
2318
2323
  } else {
2319
2324
  this.#status = "error";
@@ -2330,51 +2335,83 @@ var Agent = class {
2330
2335
  abort.abort();
2331
2336
  }
2332
2337
  }
2333
- async *#run(abort, outcome, think) {
2338
+ async *#run(abort, outcome, think, schema, limit, budget) {
2334
2339
  const messages = [...this.#context.build(this.#provider.format)];
2335
2340
  const tools = this.#context.tools;
2336
2341
  let content = "";
2337
2342
  let thinking;
2338
2343
  let usage;
2344
+ let pending = false;
2345
+ let broke = false;
2339
2346
  const compaction = { futile: false };
2340
2347
  const compacting = this.#window !== void 0 && this.#context.conversations.active?.summarizable === true;
2341
2348
  if (compacting) {
2342
2349
  this.#window?.clear();
2343
2350
  if (!abort.signal.aborted) await this.#trim(messages, compaction, false);
2344
2351
  }
2345
- for (let turn = 0; turn < this.#limit; turn += 1) {
2352
+ for (let turn = 0; turn < limit; turn += 1) {
2346
2353
  this.#emitter.emit("turn", turn);
2347
2354
  if (turn > 0) try {
2348
2355
  await this.#scheduler?.yield({ signal: abort.signal });
2349
2356
  } catch (error) {
2350
2357
  if (abort.signal.aborted) {
2351
2358
  outcome.partial = true;
2359
+ broke = true;
2352
2360
  break;
2353
2361
  }
2354
2362
  throw error;
2355
2363
  }
2356
2364
  if (abort.signal.aborted) {
2357
2365
  outcome.partial = true;
2366
+ broke = true;
2358
2367
  break;
2359
2368
  }
2360
2369
  const advertised = filterAllowList(this.#context.scope?.tools, tools.definitions(), (definition) => definition.name);
2361
2370
  const definitions = advertised.length > 0 ? advertised : void 0;
2371
+ let charged = 0;
2372
+ let turnContent = "";
2362
2373
  let result;
2363
2374
  try {
2364
- result = yield* this.#provide(messages, abort.signal, definitions, think, (delta) => {
2375
+ result = yield* this.#provide(messages, abort.signal, definitions, think, schema, (delta) => {
2365
2376
  content += delta;
2377
+ turnContent += delta;
2378
+ const est = estimateTokens(turnContent);
2379
+ if (est > charged) {
2380
+ budget?.consume({
2381
+ prompt: 0,
2382
+ completion: est - charged,
2383
+ total: est - charged
2384
+ });
2385
+ charged = est;
2386
+ }
2366
2387
  });
2367
2388
  } catch (error) {
2368
2389
  if (abort.signal.aborted) {
2369
- if (isProviderAbortError(error) && error.partial.thinking !== void 0) thinking = this.#thought(thinking, error.partial.thinking);
2390
+ if (isProviderAbortError(error)) {
2391
+ if (error.partial.thinking !== void 0) thinking = this.#thought(thinking, error.partial.thinking);
2392
+ if (error.partial.usage !== void 0) {
2393
+ const abortUsage = error.partial.usage;
2394
+ budget?.consume({
2395
+ prompt: abortUsage.prompt,
2396
+ completion: Math.max(0, abortUsage.completion - charged),
2397
+ total: Math.max(0, abortUsage.total - charged)
2398
+ });
2399
+ usage = this.#sum(usage, abortUsage);
2400
+ }
2401
+ }
2370
2402
  outcome.partial = true;
2403
+ broke = true;
2371
2404
  break;
2372
2405
  }
2373
2406
  throw error;
2374
2407
  }
2375
2408
  if (result.thinking !== void 0 && result.thinking.length > 0) thinking = this.#thought(thinking, result.thinking);
2376
2409
  if (result.usage !== void 0) {
2377
- this.#budget?.consume(result.usage);
2410
+ 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)
2414
+ });
2378
2415
  usage = this.#sum(usage, result.usage);
2379
2416
  this.#emitter.emit("usage", result.usage);
2380
2417
  yield {
@@ -2407,6 +2444,7 @@ var Agent = class {
2407
2444
  messages.push(toolMessage);
2408
2445
  }
2409
2446
  if (compacting) await this.#trim(messages, compaction, true);
2447
+ pending = true;
2410
2448
  continue;
2411
2449
  }
2412
2450
  messages.push(this.#context.messages.add({
@@ -2414,8 +2452,14 @@ var Agent = class {
2414
2452
  content: result.content
2415
2453
  }));
2416
2454
  content = result.content;
2455
+ pending = false;
2456
+ broke = true;
2417
2457
  break;
2418
2458
  }
2459
+ if (!broke && pending) {
2460
+ outcome.partial = true;
2461
+ outcome.exhausted = !abort.signal.aborted;
2462
+ }
2419
2463
  outcome.content = content;
2420
2464
  outcome.thinking = thinking;
2421
2465
  outcome.usage = usage;
@@ -2475,8 +2519,11 @@ var Agent = class {
2475
2519
  #reason(error) {
2476
2520
  return error instanceof Error ? error.message : String(error);
2477
2521
  }
2478
- async *#provide(messages, signal, definitions, think, onDelta) {
2479
- const generator = this.#provider.stream(messages, signal, definitions, think === void 0 ? void 0 : { think });
2522
+ async *#provide(messages, signal, definitions, think, schema, onDelta) {
2523
+ const options = {};
2524
+ if (think !== void 0) options.think = think;
2525
+ if (schema !== void 0) options.schema = schema;
2526
+ const generator = this.#provider.stream(messages, signal, definitions, Object.keys(options).length > 0 ? options : void 0);
2480
2527
  let next = await generator.next();
2481
2528
  while (!next.done) {
2482
2529
  const delta = next.value;
@@ -2494,11 +2541,12 @@ var Agent = class {
2494
2541
  }
2495
2542
  return next.value;
2496
2543
  }
2497
- #parents(timeout) {
2544
+ #parents(timeout, budget, signal) {
2498
2545
  const signals = [];
2499
2546
  if (this.#signal !== void 0) signals.push(this.#signal);
2547
+ if (signal !== void 0) signals.push(signal);
2500
2548
  if (timeout !== void 0) signals.push(timeout.signal);
2501
- if (this.#budget !== void 0) signals.push(this.#budget.signal);
2549
+ if (budget !== void 0) signals.push(budget.signal);
2502
2550
  if (signals.length === 0) return void 0;
2503
2551
  if (signals.length === 1) return signals[0];
2504
2552
  return AbortSignal.any(signals);