@theokit/sdk 2.1.0 → 2.3.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d7d5215: M1-3 — `buildReplayHistory(base, events, options)` pure stateless continuation-history rebuild (plan `m1-continuation-history`).
8
+
9
+ The stateless complement to M1 Phase 3's `runToCompletion` (which covers the stateful-session path). For a server / serverless handler that re-runs an agent on a fresh request and must reconstruct working memory from persisted stream events, `buildReplayHistory` serializes a round's `SDKMessage[]` into a bounded `StoredMessage[]` you can replay as prior history:
10
+
11
+ - maps assistant text → `assistant`; tool `running` → `tool_call` (args); tool `completed`/`error` → `tool_result` (carrying the result content the continued model needs);
12
+ - drops the oldest turns — pair-safe (a `tool_call` and its `tool_result` are never split) — until the total fits a context-window-derived char budget, keeping ≥ 1;
13
+ - truncates an oversized single turn (reusing the SDK's `truncateWithMarker`) rather than dropping it;
14
+ - pure, synchronous, dependency-free; a non-finite `contextWindowTokens` collapses to budget 0 (never returns an unbounded history).
15
+
16
+ Exported from `@theokit/sdk` with `ReplayHistoryOptions`. Replaces the outer-loop history rebuild a code-assistant server otherwise hand-rolls.
17
+
18
+ - f218630: M1 Phase 3 — `agent.runToCompletion()` continuation driver (plan `m1-run-to-completion`).
19
+
20
+ Builds on M1-2's `RunResult.stoppedAtIterationLimit` signal: a single `agent.send()` truncates when the model still wants tools at the loop's iteration ceiling. `runToCompletion(message, options?)` re-sends a short continuation prompt — the agent's stateful session preserves the conversation — until a genuine terminal:
21
+
22
+ - `done` — a round finished without truncating.
23
+ - `step_limit` — `maxRounds` (default 5) exhausted, or aborted via `signal`, while still truncating.
24
+ - `no_progress` — two consecutive rounds produced empty output.
25
+
26
+ Returns `{ terminal, rounds, lastResult, usage }` with token usage summed across rounds. Options: `maxRounds`, `continuationPrompt`, `onTruncated`, `signal`, `sendOptions`. Local agents only — cloud agents throw `UnsupportedRunOperationError` (the cloud runtime manages continuation server-side). This replaces the outer continuation loop a code-assistant builder would otherwise hand-roll.
27
+
28
+ ## 2.2.0
29
+
30
+ ### Minor Changes
31
+
32
+ - efe183e: M1 Reliable harness — make the agent loop's iteration ceiling real (plan `m1-reliable-harness`).
33
+
34
+ - **M1-1:** the agent loop now calls `BudgetTracker.nextIteration?.()` once per completed turn, and `nextIteration?()` is an optional member of the `BudgetTracker` interface. `createCounterBudgetTracker({ maxIterations: N })` now actually halts the loop after N turns (it was dead — nothing called it). Additive and backward-compatible: trackers that only gate on tokens/USD omit the method.
35
+ - **M1-2 (knob):** `SendOptions.maxIterations` lets a builder raise/lower the loop's default 8-turn cap per `agent.send` call. Validated at the boundary (positive integer; invalid throws `ConfigurationError`). Default of 8 preserved when unset.
36
+ - **M1-2 (truncation signal):** `RunResult.stoppedAtIterationLimit` is `true` when the loop stopped at its iteration ceiling with tool work still pending (silent truncation) vs a clean `done` finish. Lets a caller/continuation driver detect and recover.
37
+
3
38
  ## 2.1.0
4
39
 
5
40
  ### Minor Changes
@@ -2313,6 +2313,11 @@ function makeNotifier() {
2313
2313
  });
2314
2314
  return { promise, resolve: resolve3 };
2315
2315
  }
2316
+ function applyScriptMetrics(base, script) {
2317
+ if (script.usage !== void 0) base.usage = script.usage;
2318
+ if (script.cost !== void 0) base.cost = script.cost;
2319
+ if (script.stoppedAtIterationLimit === true) base.stoppedAtIterationLimit = true;
2320
+ }
2316
2321
  var FixtureRunBase;
2317
2322
  var init_fixture_run_base = __esm({
2318
2323
  "src/internal/runtime/fixtures/fixture-run-base.ts"() {
@@ -2419,8 +2424,7 @@ var init_fixture_run_base = __esm({
2419
2424
  if (status === "error" && this.script.errorDetail !== void 0) {
2420
2425
  base.error = this.script.errorDetail;
2421
2426
  }
2422
- if (this.script.usage !== void 0) base.usage = this.script.usage;
2423
- if (this.script.cost !== void 0) base.cost = this.script.cost;
2427
+ applyScriptMetrics(base, this.script);
2424
2428
  return this.extendRunResult(applyExtraRunFields(base, this.script));
2425
2429
  }
2426
2430
  /** Subclasses override to attach runtime-specific fields (e.g. cloud git info). */
@@ -2999,6 +3003,18 @@ var init_cloud_agent = __esm({
2999
3003
  "fork"
3000
3004
  );
3001
3005
  }
3006
+ /**
3007
+ * The continuation driver re-sends against a stateful local session; the
3008
+ * cloud runtime manages its own continuation policy server-side (M1 Phase 3).
3009
+ *
3010
+ * @public
3011
+ */
3012
+ runToCompletion() {
3013
+ throw new UnsupportedRunOperationError(
3014
+ "Agent.runToCompletion() is not supported on cloud agents. Cloud runtime manages continuation server-side. Use a local agent.",
3015
+ "runToCompletion"
3016
+ );
3017
+ }
3002
3018
  /**
3003
3019
  * Personality presets require consistent server-side enforcement that
3004
3020
  * the cloud runtime (pre-release) does not yet provide. Reject explicitly
@@ -8008,6 +8024,7 @@ async function runAgentLoop(inputs) {
8008
8024
  const ctx = await initLoopContext(inputs);
8009
8025
  ctxRef = ctx;
8010
8026
  const budget = inputs.budget ?? new IterationBudget({ maxIterations: inputs.maxIterations ?? 8 });
8027
+ let lastTurnDecision;
8011
8028
  while (budget.shouldContinue()) {
8012
8029
  if (inputs.budgetTracker !== void 0) {
8013
8030
  const decision2 = evaluateBudgetGate(inputs.budgetTracker);
@@ -8016,18 +8033,26 @@ async function runAgentLoop(inputs) {
8016
8033
  if (decision2.detail !== void 0) {
8017
8034
  ctx.error = { message: decision2.detail, code: decision2.reason ?? "budget" };
8018
8035
  }
8036
+ if (decision2.reason === "iteration_limit") {
8037
+ ctx.stoppedAtIterationLimit = true;
8038
+ }
8019
8039
  break;
8020
8040
  }
8021
8041
  }
8022
8042
  const usingGrace = budget.remaining <= 0 && !budget.graceCallUsed;
8023
8043
  if (usingGrace) budget.useGraceCall();
8024
8044
  const decision = await runIteration(inputs, ctx);
8045
+ lastTurnDecision = decision;
8025
8046
  if (decision === "done") break;
8026
8047
  if (decision === "error") {
8027
8048
  ctx.finalStatus = "error";
8028
8049
  break;
8029
8050
  }
8030
8051
  budget.consume();
8052
+ inputs.budgetTracker?.nextIteration?.();
8053
+ }
8054
+ if (lastTurnDecision === "continue" && budget.shouldContinue() === false) {
8055
+ ctx.stoppedAtIterationLimit = true;
8031
8056
  }
8032
8057
  if (budget.shouldContinue() === false && ctx.finalStatus === "finished" && ctx.finalText === "") {
8033
8058
  ctx.finalStatus = "error";
@@ -8058,7 +8083,8 @@ async function runAgentLoop(inputs) {
8058
8083
  conversation: ctx.conversation,
8059
8084
  ...usage !== void 0 ? { usage } : {},
8060
8085
  ...cost !== void 0 ? { cost } : {},
8061
- ...ctx.error !== void 0 ? { error: ctx.error } : {}
8086
+ ...ctx.error !== void 0 ? { error: ctx.error } : {},
8087
+ ...ctx.stoppedAtIterationLimit === true ? { stoppedAtIterationLimit: true } : {}
8062
8088
  };
8063
8089
  } finally {
8064
8090
  if (ctxRef !== void 0 && ctxRef.memoryProviderHandle !== void 0 && inputs.memoryProvider !== void 0) {
@@ -11007,6 +11033,13 @@ function resolveRunProvider(options) {
11007
11033
  return { primary, effectiveModelId };
11008
11034
  }
11009
11035
  function buildLoopInputs(options, runId, userText) {
11036
+ const maxIterations = options.sendOptions.maxIterations;
11037
+ if (maxIterations !== void 0 && (!Number.isInteger(maxIterations) || maxIterations < 1)) {
11038
+ throw new ConfigurationError(
11039
+ `SendOptions.maxIterations must be a positive integer, got ${maxIterations}`,
11040
+ { code: "invalid_max_iterations" }
11041
+ );
11042
+ }
11010
11043
  const { primary, effectiveModelId } = resolveRunProvider(options);
11011
11044
  const fallback = options.agentOptions.providers?.fallback;
11012
11045
  const apiKeys = options.agentOptions.providers?.apiKeys;
@@ -11045,6 +11078,9 @@ function buildLoopInputs(options, runId, userText) {
11045
11078
  // D318 — forward SendOptions.signal to the agent loop so streamLlmTurn
11046
11079
  // can attach it to the LLM `fetch({ signal })` call.
11047
11080
  ...options.sendOptions.signal !== void 0 ? { signal: options.sendOptions.signal } : {},
11081
+ // M1-2: per-send iteration ceiling (validated above). The loop reads
11082
+ // inputs.maxIterations (default 8 when unset).
11083
+ ...maxIterations !== void 0 ? { maxIterations } : {},
11048
11084
  // D315-D317 — tool lifecycle hooks (cost tracking + audit + retry/alert)
11049
11085
  ...options.agentOptions.onToolStart !== void 0 ? { onToolStart: options.agentOptions.onToolStart } : {},
11050
11086
  ...options.agentOptions.onToolEnd !== void 0 ? { onToolEnd: options.agentOptions.onToolEnd } : {},
@@ -11104,6 +11140,7 @@ function buildMcpMap(options) {
11104
11140
  var pluginProvidersAnnounced, RealLocalRun;
11105
11141
  var init_real_local_run = __esm({
11106
11142
  "src/internal/runtime/local-agent/real-local-run.ts"() {
11143
+ init_errors();
11107
11144
  init_loop();
11108
11145
  init_fallback_client();
11109
11146
  init_model_identifier();
@@ -11191,6 +11228,7 @@ var init_real_local_run = __esm({
11191
11228
  if (output.result.length > 0) this.script.result = output.result;
11192
11229
  if (output.usage !== void 0) this.script.usage = output.usage;
11193
11230
  if (output.cost !== void 0) this.script.cost = output.cost;
11231
+ if (output.stoppedAtIterationLimit === true) this.script.stoppedAtIterationLimit = true;
11194
11232
  if (output.error !== void 0 && this.script.errorDetail === void 0) {
11195
11233
  this.script.errorDetail = {
11196
11234
  message: output.error.message,
@@ -14164,6 +14202,71 @@ var init_agent_factory_registry = __esm({
14164
14202
  }
14165
14203
  });
14166
14204
 
14205
+ // src/internal/runtime/lifecycle/run-to-completion.ts
14206
+ var run_to_completion_exports = {};
14207
+ __export(run_to_completion_exports, {
14208
+ classifyRound: () => classifyRound,
14209
+ runToCompletionImpl: () => runToCompletionImpl
14210
+ });
14211
+ function isEmptyRound(result) {
14212
+ return (result.result ?? "").trim() === "";
14213
+ }
14214
+ function classifyRound(result, round, maxRounds, emptyStreak) {
14215
+ if (result.stoppedAtIterationLimit !== true) return "done";
14216
+ if (isEmptyRound(result) && emptyStreak >= 1) return "no_progress";
14217
+ if (round >= maxRounds) return "step_limit";
14218
+ return "continue";
14219
+ }
14220
+ function addUsage(acc, u) {
14221
+ if (u === void 0) return acc;
14222
+ const inputTokens = (acc?.inputTokens ?? 0) + u.inputTokens;
14223
+ const outputTokens = (acc?.outputTokens ?? 0) + u.outputTokens;
14224
+ const sumOpt = (a, b) => a === void 0 && b === void 0 ? void 0 : (a ?? 0) + (b ?? 0);
14225
+ return {
14226
+ inputTokens,
14227
+ outputTokens,
14228
+ totalTokens: inputTokens + outputTokens,
14229
+ cacheReadTokens: sumOpt(acc?.cacheReadTokens, u.cacheReadTokens),
14230
+ cacheWriteTokens: sumOpt(acc?.cacheWriteTokens, u.cacheWriteTokens),
14231
+ reasoningTokens: sumOpt(acc?.reasoningTokens, u.reasoningTokens)
14232
+ };
14233
+ }
14234
+ function buildResult(terminal, rounds, lastResult, usage) {
14235
+ return { terminal, rounds, lastResult, ...usage !== void 0 ? { usage } : {} };
14236
+ }
14237
+ async function stepRound(agent, prompt, sendOptions, round, maxRounds, state2) {
14238
+ const run = await agent.send(prompt, sendOptions);
14239
+ const result = await run.wait();
14240
+ const usage = addUsage(state2.usage, result.usage);
14241
+ const decision = classifyRound(result, round, maxRounds, state2.emptyStreak);
14242
+ if (decision !== "continue") return { terminal: buildResult(decision, round, result, usage) };
14243
+ const emptyStreak = isEmptyRound(result) ? state2.emptyStreak + 1 : 0;
14244
+ return { next: { usage, emptyStreak }, lastResult: result };
14245
+ }
14246
+ async function runToCompletionImpl(agent, message, options) {
14247
+ const maxRounds = options?.maxRounds ?? DEFAULT_MAX_ROUNDS;
14248
+ const continuationPrompt = options?.continuationPrompt ?? DEFAULT_CONTINUATION_PROMPT;
14249
+ const { onTruncated, signal, sendOptions } = options ?? {};
14250
+ let state2 = { usage: void 0, emptyStreak: 0 };
14251
+ for (let round = 0; ; round += 1) {
14252
+ const prompt = round === 0 ? message : continuationPrompt;
14253
+ const outcome = await stepRound(agent, prompt, sendOptions, round, maxRounds, state2);
14254
+ if ("terminal" in outcome) return outcome.terminal;
14255
+ state2 = outcome.next;
14256
+ await onTruncated?.({ round });
14257
+ if (signal?.aborted === true) {
14258
+ return buildResult("step_limit", round, outcome.lastResult, state2.usage);
14259
+ }
14260
+ }
14261
+ }
14262
+ var DEFAULT_MAX_ROUNDS, DEFAULT_CONTINUATION_PROMPT;
14263
+ var init_run_to_completion = __esm({
14264
+ "src/internal/runtime/lifecycle/run-to-completion.ts"() {
14265
+ DEFAULT_MAX_ROUNDS = 5;
14266
+ DEFAULT_CONTINUATION_PROMPT = "Continue from where you left off and finish the task. If it is already complete, give the final answer.";
14267
+ }
14268
+ });
14269
+
14167
14270
  // src/internal/runtime/lifecycle/fork-agent.ts
14168
14271
  var fork_agent_exports = {};
14169
14272
  __export(fork_agent_exports, {
@@ -14244,6 +14347,13 @@ function localAgentRunUntil(agent, goal, options) {
14244
14347
  }
14245
14348
  return wrap();
14246
14349
  }
14350
+ function localAgentRunToCompletion(agent, message, options) {
14351
+ async function run() {
14352
+ const { runToCompletionImpl: runToCompletionImpl2 } = await Promise.resolve().then(() => (init_run_to_completion(), run_to_completion_exports));
14353
+ return runToCompletionImpl2({ send: (m, o) => agent.send(m, o) }, message, options);
14354
+ }
14355
+ return run();
14356
+ }
14247
14357
  async function localAgentFork(parent, options) {
14248
14358
  const { forkAgentImpl: forkAgentImpl2 } = await Promise.resolve().then(() => (init_fork_agent(), fork_agent_exports));
14249
14359
  const { getAgentFacade: getAgentFacade2 } = await Promise.resolve().then(() => (init_agent_factory_registry(), agent_factory_registry_exports));
@@ -15378,6 +15488,10 @@ var init_local_agent = __esm({
15378
15488
  fork(options) {
15379
15489
  return localAgentFork({ agentId: this.agentId, options: this.options, personalitySlugSnapshot: this.personalityStore.active(this.agentId) }, options);
15380
15490
  }
15491
+ // biome-ignore format: G8 budget — see runUntil comment above.
15492
+ runToCompletion(message, options) {
15493
+ return localAgentRunToCompletion(this, message, options);
15494
+ }
15381
15495
  };
15382
15496
  }
15383
15497
  });