@theokit/sdk 2.2.0 → 2.4.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.
Files changed (85) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/dist/a2a/index.cjs +191 -48
  3. package/dist/a2a/index.cjs.map +1 -1
  4. package/dist/a2a/index.js +192 -49
  5. package/dist/a2a/index.js.map +1 -1
  6. package/dist/compaction.cjs +78 -0
  7. package/dist/compaction.cjs.map +1 -0
  8. package/dist/compaction.d.cts +76 -0
  9. package/dist/compaction.d.ts +76 -0
  10. package/dist/compaction.js +70 -0
  11. package/dist/compaction.js.map +1 -0
  12. package/dist/{cron-JSPSFczQ.d.cts → cron-B656C3iq.d.cts} +28 -2
  13. package/dist/{cron-Aksw2Hy4.d.ts → cron-CM2M9mhB.d.ts} +28 -2
  14. package/dist/cron.cjs +192 -57
  15. package/dist/cron.cjs.map +1 -1
  16. package/dist/cron.d.cts +2 -2
  17. package/dist/cron.d.ts +2 -2
  18. package/dist/cron.js +192 -57
  19. package/dist/cron.js.map +1 -1
  20. package/dist/{errors-Bcw_Pakm.d.ts → errors-DG_7CAUg.d.ts} +1 -1
  21. package/dist/{errors-Vhg6ZV4o.d.cts → errors-QDYUPABr.d.cts} +1 -1
  22. package/dist/errors.d.cts +2 -2
  23. package/dist/eval.cjs +192 -57
  24. package/dist/eval.cjs.map +1 -1
  25. package/dist/eval.js +192 -57
  26. package/dist/eval.js.map +1 -1
  27. package/dist/index.cjs +275 -68
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +50 -7
  30. package/dist/index.d.ts +50 -7
  31. package/dist/index.js +275 -69
  32. package/dist/index.js.map +1 -1
  33. package/dist/internal/agent-loop/loop.d.ts +5 -0
  34. package/dist/internal/llm/model-capabilities.d.ts +40 -0
  35. package/dist/internal/llm/model-identifier.d.ts +9 -1
  36. package/dist/internal/llm/model-option.d.ts +38 -0
  37. package/dist/internal/runtime/compression/compression-attempt.d.ts +24 -0
  38. package/dist/internal/runtime/compression/compression-config.d.ts +33 -0
  39. package/dist/internal/runtime/compression/compression-decision.d.ts +10 -0
  40. package/dist/internal/runtime/compression/compression-helpers.d.ts +18 -0
  41. package/dist/internal/runtime/compression/compression-model-registry.d.ts +41 -0
  42. package/dist/internal/runtime/compression/compression-summarizer.d.ts +29 -0
  43. package/dist/internal/runtime/context/project-instructions.d.ts +66 -0
  44. package/dist/internal/runtime/context/replay-history.d.ts +43 -0
  45. package/dist/internal/runtime/hooks/hooks-frontmatter.d.ts +1 -1
  46. package/dist/internal/runtime/lifecycle/run-to-completion.d.ts +22 -0
  47. package/dist/internal/runtime/skills/discover-skills.d.ts +68 -0
  48. package/dist/internal/runtime/skills/skills-block.d.ts +18 -0
  49. package/dist/internal/runtime/skills/subagent-tool-scope.d.ts +25 -0
  50. package/dist/messages.cjs +24 -0
  51. package/dist/messages.cjs.map +1 -0
  52. package/dist/messages.d.cts +33 -0
  53. package/dist/messages.d.ts +33 -0
  54. package/dist/messages.js +20 -0
  55. package/dist/messages.js.map +1 -0
  56. package/dist/models.cjs +233 -0
  57. package/dist/models.cjs.map +1 -0
  58. package/dist/models.d.cts +16 -0
  59. package/dist/models.d.ts +16 -0
  60. package/dist/models.js +228 -0
  61. package/dist/models.js.map +1 -0
  62. package/dist/project.cjs +149 -0
  63. package/dist/project.cjs.map +1 -0
  64. package/dist/project.d.cts +14 -0
  65. package/dist/project.d.ts +14 -0
  66. package/dist/project.js +146 -0
  67. package/dist/project.js.map +1 -0
  68. package/dist/{run-ekGKZlmg.d.cts → run-BPRYG1Id.d.cts} +55 -2
  69. package/dist/{run-ekGKZlmg.d.ts → run-BPRYG1Id.d.ts} +55 -2
  70. package/dist/skills.cjs +282 -0
  71. package/dist/skills.cjs.map +1 -0
  72. package/dist/skills.d.cts +19 -0
  73. package/dist/skills.d.ts +19 -0
  74. package/dist/skills.js +279 -0
  75. package/dist/skills.js.map +1 -0
  76. package/dist/subagents.cjs +24 -0
  77. package/dist/subagents.cjs.map +1 -0
  78. package/dist/subagents.d.cts +14 -0
  79. package/dist/subagents.d.ts +14 -0
  80. package/dist/subagents.js +21 -0
  81. package/dist/subagents.js.map +1 -0
  82. package/dist/types/agent.d.ts +22 -0
  83. package/dist/types/conversation-storage.d.ts +5 -1
  84. package/dist/types/run.d.ts +54 -1
  85. package/package.json +62 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,116 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a21949f: M1-5 — `@theokit/sdk/messages`: pure readers over the `SDKMessage` stream (plan `m1-sdkmessage-readers`).
8
+
9
+ Consumers reading the `SDKMessage` stream had to hand-roll a wire-event mapper. The SDK now ships three pure readers on a dedicated sub-path, promoting the proven first-party hand-roll onto the SDK's own types:
10
+
11
+ - `assistantText(msg)` — concatenates an assistant message's `text` blocks; `""` for any non-assistant message (or one with no text). `tool_use` blocks are ignored.
12
+ - `extractToolUses(msg)` — returns the assistant message's `ToolUseBlock`s; `[]` for non-assistant. Reads the assistant content blocks, NOT the separate `SDKToolUseMessage` (`type:"tool_call"`) lifecycle event.
13
+ - `costAmountUsd(cost)` — reads `RunResult.cost.amountUsd` preserving `number | undefined` verbatim. An unknown cost stays `undefined` (never coerced to `$0`), distinct from a real `$0` subscription-included route — the cost-honesty contract (ADR D377).
14
+
15
+ All three are pure (no I/O, inputs never mutated). Zero new dependencies.
16
+
17
+ - fb268f9: M1-4 — fire the `stop` file-based hook + honor `feedback` as a bounded re-prompt (plan `m1-stop-hook-reflection`).
18
+
19
+ The `HookEvent "stop"` was declared but never dispatched. A local agent now fires `stop` once when it finishes a turn cleanly (not on an errored run or an iteration-ceiling truncation). A `stop` hook returning `{"decision":"feedback","feedback":"…"}` re-prompts the agent with that text and the loop continues — a bounded reflection ladder capped at `MAX_STOP_FEEDBACK_ATTEMPTS` (2), mirroring the existing nudge ceiling, so a hook cannot loop forever. `allow`/no-hook finish normally; `deny` at `stop` finishes (the answer already exists). Reuses the existing `HooksExecutor` — zero new dependencies. Hooks remain file-based (no programmatic callback).
20
+
21
+ - 5b8c9e7: M2-1 — `@theokit/sdk/compaction`: public compaction / context-management helpers (plan `m2-compaction-public-api`).
22
+
23
+ Promotes the SDK's compaction capability to a public sub-path so consumers can manage the context window without reaching into `internal/`:
24
+
25
+ - `compactTranscript(messages, { keepRecent = 6, summarize? })` — keep the last `keepRecent` turns verbatim, preserve leading system turns, and either summarize the older window (via an optional `summarize` callback that can wire the SDK's internal LLM summarizer) or drop it. Reuses the internal `selectCompressionWindow` (no second algorithm). Never mutates its input.
26
+ - `buildCheckpoint(label?)` / `filterFromLatestCheckpoint(messages)` / `CHECKPOINT_MARKER` — a string-sentinel checkpoint: mark a point in a transcript and later filter back to the turns after the most recent marker.
27
+ - `isContextOverflowError(err)` — true iff `err` is a `TheokitAgentError` (or subclass) reporting the typed `context_too_long` code (reads both `err.code` and `err.metadata?.code`; no brittle message regex).
28
+
29
+ Operates on the SDK's own `CompressibleMessage` type (re-exported). Zero new dependencies.
30
+
31
+ - 1cf9c16: M2-4 — per-model capability catalog public + OpenRouter slug-suffix fix (plan `m2-model-capabilities`).
32
+
33
+ - **New `@theokit/sdk/models` subpath.** `resolveModelCapabilities(modelId): ModelCapabilities` (previously dead `@internal`) is now public — returns a model's capability flags + `maxContextTokens`/`maxOutputTokens` from a static, OFFLINE catalog (pure, sync, no network). Pair `maxContextTokens` with `@theokit/sdk/compaction`'s `shouldCompact`.
34
+ - **Fix:** OpenRouter `:variant` suffixes (`:free`/`:nitro`/`:floor`/`:beta`) were not stripped before the catalog lookup, so `openrouter/openai/gpt-4o:free` fell back to conservative defaults (4096) instead of the real 128k window. The suffix is now stripped (alongside the existing routing-prefix strip); unknown models still get conservative defaults.
35
+
36
+ Zero new dependencies.
37
+
38
+ - b31283c: M2-2 — pre-call token estimate + compaction decision (plan `m2-token-estimate`).
39
+
40
+ Two pure, zero-dependency helpers on the `@theokit/sdk/compaction` subpath (siblings of `compactTranscript`/`isContextOverflowError`):
41
+
42
+ - `estimateTokens(text)` — a tokenizer-free token estimate via the ~4-chars-per-token heuristic (`ceil(text.length / 4)`): `""` → 0, any non-empty text → ≥ 1. A cheap PRE-CALL gate, not exact tokenization.
43
+ - `shouldCompact({ estimated, contextWindow, buffer })` — decide BEFORE sending whether to compact: `true` when `estimated >= contextWindow - buffer`. Pure; the caller supplies the window (e.g. from `resolveModelCapabilities`), keeping it decoupled from any per-model catalog.
44
+
45
+ No tokenizer dependency.
46
+
47
+ - 29b1c8c: M4-2 — hierarchical project-instruction reader/writer (plan `m4-project-instructions`).
48
+
49
+ New `@theokit/sdk/project` subpath:
50
+
51
+ - `readProjectInstructions(cwd, options?)` — walk up from `cwd` collecting `<dir>/<filename>` (default `THEO.md`; configurable) up to the filesystem root (or `options.stopDir`). Returns `{ files, content }`: `files` are the found files nearest-first (`{ path, content }[]`, read in full), `content` is a reduction chosen by `options.scope` — `"nearest"` (innermost) or `"merged"` (all joined root-first, nearest text last). NEVER throws — missing/unreadable/non-file paths are skipped.
52
+ - `writeProjectInstructions(cwd, content, options?)` — write `<cwd>/<filename>` atomically (temp + fsync + rename). Fails loud on write errors (unlike the best-effort reader).
53
+
54
+ Composes the SDK's hardened `walkUpForFile` discovery + the atomic `replaceFileAtomic` writer (Rule 9). Zero new dependencies.
55
+
56
+ - f9be17a: M4-1 — first-party skill discovery + `<skills>` block (plan `m4-skills-discovery`).
57
+
58
+ New `@theokit/sdk/skills` subpath exposing two pure first-party primitives the SDK runtime already uses internally:
59
+
60
+ - `discoverSkills(dir, options?)` — discover `<dir>/<name>/SKILL.md` files under an ARBITRARY directory (not a hardcoded `.theokit/skills` root), parsing strict YAML frontmatter (`name`/`description` required; `category`/`dependencies` optional) and returning `Skill[]` (the skill BODY is never included). A subdirectory whose realpath escapes `dir` via symlink is skipped (symlink-escape guard, reusing `@theokit/sdk/path-safety`). NEVER throws — a missing/unreadable/non-directory path yields `[]`. A `SKILL.md` with malformed frontmatter is excluded and optionally reported via `options.onInvalidSkill`; a directory WITHOUT a `SKILL.md` is silently skipped.
61
+ - `buildSkillsBlock(skills)` — render the prompt-injection-safe `<skills>` system-prompt block (name + description XML-escaped); returns `undefined` for an empty list.
62
+
63
+ The internal `SkillsManager` (`.theokit/skills` discovery) and `SkillsPromptProvider` (`<skills>` injection) now delegate to these primitives — single source of truth, behavior preserved (golden + contract tests unchanged). Zero new dependencies.
64
+
65
+ - f2265d7: M4-6 — sub-agent tool scoping via `AgentDefinition.tools` (plan `m4-tool-scoping`).
66
+
67
+ - `AgentDefinition` gains an optional `tools?: string[]` — a tool-name whitelist. When set, the sub-agent may ONLY call tools whose canonical (post-repair, lowercase) name is in the list; any other call is vetoed at dispatch. Absent/empty → unscoped (inherits the parent's full toolset). Backward-compatible.
68
+ - `.theokit/agents/*.md` subagents can declare it as a comma/space-separated frontmatter field (`tools: read_file, list_dir`).
69
+ - New `@theokit/sdk/subagents` subpath: `subagentToolWhitelist(definition): Set<string> | undefined` + `withSubagentToolScope(definition, fn)` enforce the whitelist via the SDK's existing `withToolWhitelist` dispatch veto — the same enforcement `Agent.fork`'s `allowedTools` uses, NOT `PermissionEngine`. A `tools: ["read_file"]` sub-agent provably cannot call `write_file`/`shell_exec`.
70
+
71
+ Zero new dependencies.
72
+
73
+ - f1de451: M5-8 — public `parseModelId` + `humanizeModelName` + `toModelOption` on `@theokit/sdk/models` (plan `m5-model-option`).
74
+
75
+ - `parseModelId(modelId): { provider, name }` is now public (promoted from `@internal`) — splits the provider prefix from the model name, OpenRouter-routing + tag-suffix aware.
76
+ - `humanizeModelName(modelId): string` — a best-effort, deterministic human label: strips the routing/vendor prefix, title-cases the core model segment (known acronyms upper-cased), and appends an OpenRouter `:variant` in parens (`"openrouter/openai/gpt-4o:free"` → `"GPT 4o (free)"`). Not vendor-canonical marketing names.
77
+ - `toModelOption(modelId): { value, label, provider }` — a dropdown-ready entry composing the two.
78
+
79
+ Lets `@theokit/ui` model selectors + the `create-theokit` template stop hand-rolling slug→label. Zero new dependencies.
80
+
81
+ ### Patch Changes
82
+
83
+ - 1abda16: M2-3 — `context_too_long` reaches the run boundary (plan `m2-context-overflow-boundary`).
84
+
85
+ Fixes a code-at-boundary bug: the loop captured the error code from the error's top-level `.code`, which the provider mappers set to a PROVIDER-PREFIXED string (`anthropic_context_too_long` / `${providerId}_context_too_long`), while the CANONICAL `ErrorCode` (`context_too_long`) lives on `metadata.code`. So `RunResult.error.code` surfaced the prefixed form and a consumer checking `result.error.code === "context_too_long"` missed it.
86
+
87
+ `registerLoopError` now prefers `cause.metadata?.code` over the top-level `.code`, so the canonical code reaches the boundary for every provider (verified by a 400-context-overflow contract test through the real `mapAnthropicError`/`mapOpenAICompatibleError`). The prefixed form remains on the thrown `TheokitAgentError.code` for telemetry. Set-once invariant preserved; top-level `.code` fallback unchanged when there is no `metadata.code`.
88
+
89
+ ## 2.3.0
90
+
91
+ ### Minor Changes
92
+
93
+ - d7d5215: M1-3 — `buildReplayHistory(base, events, options)` pure stateless continuation-history rebuild (plan `m1-continuation-history`).
94
+
95
+ 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:
96
+
97
+ - maps assistant text → `assistant`; tool `running` → `tool_call` (args); tool `completed`/`error` → `tool_result` (carrying the result content the continued model needs);
98
+ - 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;
99
+ - truncates an oversized single turn (reusing the SDK's `truncateWithMarker`) rather than dropping it;
100
+ - pure, synchronous, dependency-free; a non-finite `contextWindowTokens` collapses to budget 0 (never returns an unbounded history).
101
+
102
+ Exported from `@theokit/sdk` with `ReplayHistoryOptions`. Replaces the outer-loop history rebuild a code-assistant server otherwise hand-rolls.
103
+
104
+ - f218630: M1 Phase 3 — `agent.runToCompletion()` continuation driver (plan `m1-run-to-completion`).
105
+
106
+ 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:
107
+
108
+ - `done` — a round finished without truncating.
109
+ - `step_limit` — `maxRounds` (default 5) exhausted, or aborted via `signal`, while still truncating.
110
+ - `no_progress` — two consecutive rounds produced empty output.
111
+
112
+ 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.
113
+
3
114
  ## 2.2.0
4
115
 
5
116
  ### Minor Changes
@@ -67,8 +178,19 @@
67
178
 
68
179
  ## [Unreleased]
69
180
 
181
+ ### Fixed
182
+
183
+ - **`context_too_long` reaches the run boundary (M2-3).** `registerLoopError` now prefers the canonical `cause.metadata?.code` over the provider-prefixed top-level `.code`, so `RunResult.error.code` is `context_too_long` (not `anthropic_context_too_long`) for every provider. Set-once + top-level fallback preserved.
184
+
185
+ ### Added
186
+
187
+ - **Pre-call token estimate + compaction decision (M2-2).** `estimateTokens(text)` (tokenizer-free ~4-chars/token; `""`→0, non-empty→≥1) + `shouldCompact({estimated,contextWindow,buffer})` (`true` when `estimated >= contextWindow - buffer`; pure, caller supplies the window) on the `@theokit/sdk/compaction` subpath. No tokenizer dep.
188
+ - **Per-model capability catalog public + OpenRouter slug-suffix fix (M2-4).** New `@theokit/sdk/models` subpath: `resolveModelCapabilities(modelId)` (was `@internal`) — pure/sync/offline capability flags + `maxContextTokens`/`maxOutputTokens`. Fixes an OpenRouter `:variant` suffix lookup miss (fell back to 4096 instead of the real window).
189
+
70
190
  ### Added
71
191
 
192
+ - `@theokit/sdk/compaction` — public compaction / context-management helpers so you manage the context window without reaching into `internal/`. `compactTranscript(messages, { keepRecent = 6, summarize? })` keeps the last `keepRecent` turns, preserves leading system turns, and either summarizes the older window (via an optional callback wiring the internal LLM summarizer) or drops it — reusing the internal compaction window (no second algorithm), never mutating its input. `buildCheckpoint`/`filterFromLatestCheckpoint`/`CHECKPOINT_MARKER` give a string-sentinel checkpoint to bound replay to "since the last checkpoint". `isContextOverflowError(err)` is true for a `TheokitAgentError` reporting the typed `context_too_long` code (checks `code` + `metadata.code`; no message regex). Operates on the SDK's own `CompressibleMessage` (re-exported); zero new dependencies. (M2-1)
193
+ - `@theokit/sdk/messages` — pure readers over the `SDKMessage` stream so you stop hand-rolling a wire-event mapper. `assistantText(msg)` concatenates an assistant message's text (`""` for non-assistant), `extractToolUses(msg)` returns its tool-use blocks (`[]` for non-assistant; reads the assistant content blocks, not the separate `tool_call` lifecycle event), and `costAmountUsd(cost)` reads `RunResult.cost.amountUsd` preserving `number | undefined` verbatim — an unknown cost stays `undefined`, never silently coerced to `$0` (cost-honesty, ADR D377). Zero new dependencies. (#34)
72
194
  - `createSquad({ agents })` — a thin convenience for sequential agent teams. Runs agents in order, threading each output into the next agent's prompt; returns `{ result, status, steps }`. Composes `Workflow` + `agentStep` internally (no new orchestration engine). `process: "hierarchical"` throws a guiding `ConfigurationError` (use subagents / `@theokit/sdk-handoff`); empty `agents` → `ConfigurationError(code: "invalid_squad")`.
73
195
 
74
196
  ### Fixed
@@ -3003,6 +3003,18 @@ var init_cloud_agent = __esm({
3003
3003
  "fork"
3004
3004
  );
3005
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
+ }
3006
3018
  /**
3007
3019
  * Personality presets require consistent server-side enforcement that
3008
3020
  * the cloud runtime (pre-release) does not yet provide. Reject explicitly
@@ -4910,6 +4922,8 @@ function parseSubagentMarkdown(raw, filename) {
4910
4922
  if (fields.model !== void 0) {
4911
4923
  definition.model = fields.model === "inherit" ? "inherit" : { id: fields.model };
4912
4924
  }
4925
+ const tools = fields.tools?.split(/[\s,]+/).map((t) => t.trim()).filter((t) => t.length > 0);
4926
+ if (tools !== void 0 && tools.length > 0) definition.tools = tools;
4913
4927
  return { name, definition };
4914
4928
  }
4915
4929
  function splitFrontmatter2(raw, filename) {
@@ -5113,25 +5127,33 @@ ${lines.join("\n")}
5113
5127
  }
5114
5128
  });
5115
5129
 
5130
+ // src/internal/runtime/skills/skills-block.ts
5131
+ function buildSkillsBlock(skills) {
5132
+ if (skills.length === 0) return void 0;
5133
+ const lines = skills.map(
5134
+ (skill) => ` - ${escapeBlockBody(skill.name)}: ${escapeBlockBody(skill.description)}`
5135
+ );
5136
+ return `<skills>
5137
+ ${lines.join("\n")}
5138
+ </skills>`;
5139
+ }
5140
+ var init_skills_block = __esm({
5141
+ "src/internal/runtime/skills/skills-block.ts"() {
5142
+ init_escape();
5143
+ }
5144
+ });
5145
+
5116
5146
  // src/internal/runtime/system-prompt/sources/skills-provider.ts
5117
5147
  var SkillsPromptProvider;
5118
5148
  var init_skills_provider = __esm({
5119
5149
  "src/internal/runtime/system-prompt/sources/skills-provider.ts"() {
5120
- init_escape();
5150
+ init_skills_block();
5121
5151
  SkillsPromptProvider = class {
5122
5152
  id = "skills";
5123
5153
  priority = 20;
5124
5154
  contribute(ctx) {
5125
5155
  if (ctx.skillsAutoInject === false) return Promise.resolve(void 0);
5126
- if (ctx.skills.length === 0) return Promise.resolve(void 0);
5127
- const lines = ctx.skills.map((skill) => {
5128
- const name = escapeBlockBody(skill.name);
5129
- const description = escapeBlockBody(skill.description);
5130
- return ` - ${name}: ${description}`;
5131
- });
5132
- return Promise.resolve(`<skills>
5133
- ${lines.join("\n")}
5134
- </skills>`);
5156
+ return Promise.resolve(buildSkillsBlock(ctx.skills));
5135
5157
  }
5136
5158
  };
5137
5159
  }
@@ -6387,36 +6409,71 @@ var init_skill_frontmatter = __esm({
6387
6409
  init_yaml_frontmatter();
6388
6410
  }
6389
6411
  });
6390
- function tryParseSkill(raw, fallbackName, source) {
6412
+ async function discoverSkills(dir, options) {
6413
+ let entries;
6414
+ try {
6415
+ entries = await readWorkspaceDir(dir, "skills_read_error", "skills directory");
6416
+ } catch {
6417
+ return [];
6418
+ }
6419
+ const skills = [];
6420
+ for (const entry of entries) {
6421
+ if (!entry.isDirectory()) continue;
6422
+ let skillDir;
6423
+ try {
6424
+ skillDir = safePathJoin(dir, entry.name);
6425
+ assertNoSymlinkEscape(skillDir, dir);
6426
+ } catch {
6427
+ continue;
6428
+ }
6429
+ const skillPath = path.join(skillDir, "SKILL.md");
6430
+ let raw;
6431
+ try {
6432
+ raw = await promises.readFile(skillPath, "utf8");
6433
+ } catch {
6434
+ continue;
6435
+ }
6436
+ const skill = tryParseSkill(raw, entry.name, skillPath, options);
6437
+ if (skill !== void 0) skills.push(skill);
6438
+ }
6439
+ return skills;
6440
+ }
6441
+ function tryParseSkill(raw, fallbackName, source, options) {
6391
6442
  try {
6392
6443
  const frontmatter = parseSkillFrontmatter(raw, fallbackName);
6393
- const metadata = {
6444
+ const skill = {
6394
6445
  name: frontmatter.name,
6395
6446
  description: frontmatter.description,
6396
6447
  source
6397
6448
  };
6398
- if (frontmatter.category !== void 0) metadata.category = frontmatter.category;
6399
- if (frontmatter.dependencies !== void 0) metadata.dependencies = frontmatter.dependencies;
6400
- return metadata;
6449
+ if (frontmatter.category !== void 0) skill.category = frontmatter.category;
6450
+ if (frontmatter.dependencies !== void 0) skill.dependencies = frontmatter.dependencies;
6451
+ return skill;
6401
6452
  } catch (cause) {
6402
6453
  if (cause instanceof ConfigurationError) {
6403
- const code = cause.code ?? "unknown";
6404
- process.stderr.write(
6405
- `[theokit-sdk] skill ${fallbackName} skipped (${code}): ${cause.message}
6406
- `
6407
- );
6454
+ options?.onInvalidSkill?.({
6455
+ name: fallbackName,
6456
+ source,
6457
+ code: cause.code ?? "unknown",
6458
+ message: cause.message
6459
+ });
6408
6460
  return void 0;
6409
6461
  }
6410
6462
  throw cause;
6411
6463
  }
6412
6464
  }
6413
- var SkillsManager;
6414
- var init_skills_manager = __esm({
6415
- "src/internal/runtime/skills/skills-manager.ts"() {
6465
+ var init_discover_skills = __esm({
6466
+ "src/internal/runtime/skills/discover-skills.ts"() {
6416
6467
  init_errors();
6417
6468
  init_path_guard();
6418
6469
  init_workspace_dir();
6419
6470
  init_skill_frontmatter();
6471
+ }
6472
+ });
6473
+ var SkillsManager;
6474
+ var init_skills_manager = __esm({
6475
+ "src/internal/runtime/skills/skills-manager.ts"() {
6476
+ init_discover_skills();
6420
6477
  SkillsManager = class {
6421
6478
  constructor(cwd, _enabled, settingSourcesIncludeProject) {
6422
6479
  this.cwd = cwd;
@@ -6433,28 +6490,15 @@ var init_skills_manager = __esm({
6433
6490
  await this.refresh();
6434
6491
  }
6435
6492
  async refresh() {
6436
- this.skills = [];
6437
6493
  const skillsRoot = path.join(this.cwd, ".theokit", "skills");
6438
- const entries = await readWorkspaceDir(skillsRoot, "skills_read_error", "skills directory");
6439
- for (const entry of entries) {
6440
- if (!entry.isDirectory()) continue;
6441
- let skillDir;
6442
- try {
6443
- skillDir = safePathJoin(skillsRoot, entry.name);
6444
- assertNoSymlinkEscape(skillDir, skillsRoot);
6445
- } catch {
6446
- continue;
6447
- }
6448
- const skillPath = path.join(skillDir, "SKILL.md");
6449
- let raw;
6450
- try {
6451
- raw = await promises.readFile(skillPath, "utf8");
6452
- } catch {
6453
- continue;
6494
+ this.skills = await discoverSkills(skillsRoot, {
6495
+ onInvalidSkill: (info) => {
6496
+ process.stderr.write(
6497
+ `[theokit-sdk] skill ${info.name} skipped (${info.code}): ${info.message}
6498
+ `
6499
+ );
6454
6500
  }
6455
- const metadata = tryParseSkill(raw, entry.name, skillPath);
6456
- if (metadata !== void 0) this.skills.push(metadata);
6457
- }
6501
+ });
6458
6502
  }
6459
6503
  list() {
6460
6504
  return Promise.resolve(this.skills);
@@ -6916,6 +6960,7 @@ async function initLoopContext(inputs) {
6916
6960
  finalStatus: "finished",
6917
6961
  usage: new UsageAccumulator(),
6918
6962
  nudgeAttempts: 0,
6963
+ stopFeedbackAttempts: 0,
6919
6964
  ...memoryProviderHandle !== void 0 ? { memoryProviderHandle } : {},
6920
6965
  ...memorySystemPromptAdditions !== void 0 ? { memorySystemPromptAdditions } : {}
6921
6966
  };
@@ -7071,8 +7116,9 @@ function registerLoopError(ctx, cause) {
7071
7116
  if (ctx.error !== void 0) return;
7072
7117
  const rawMessage = cause?.message;
7073
7118
  const message = typeof rawMessage === "string" ? rawMessage : cause instanceof Error ? cause.message : String(cause);
7119
+ const metaCode = cause?.metadata?.code;
7074
7120
  const rawCode = cause?.code;
7075
- const code = typeof rawCode === "string" ? rawCode : void 0;
7121
+ const code = typeof metaCode === "string" ? metaCode : typeof rawCode === "string" ? rawCode : void 0;
7076
7122
  ctx.error = code !== void 0 ? { message, code, cause } : { message, cause };
7077
7123
  }
7078
7124
  async function runCollectorLoop(generator, inputs, ctx) {
@@ -8155,6 +8201,28 @@ function shouldNudgeAndContinue(ctx, llmOutput) {
8155
8201
  });
8156
8202
  return true;
8157
8203
  }
8204
+ async function reflectAfterStop(inputs, ctx) {
8205
+ const result = await inputs.hooks.run({
8206
+ event: "stop",
8207
+ agentId: inputs.agentId,
8208
+ runId: inputs.runId
8209
+ });
8210
+ if (result.blocked) return false;
8211
+ if (ctx.stopFeedbackAttempts >= MAX_STOP_FEEDBACK_ATTEMPTS) return false;
8212
+ const feedback = result.decisions.find(
8213
+ (d) => d.decision === "feedback" && (d.feedback ?? "").length > 0
8214
+ )?.feedback;
8215
+ if (feedback === void 0) return false;
8216
+ ctx.stopFeedbackAttempts += 1;
8217
+ ctx.messages.push({ role: "user", content: [{ type: "text", text: feedback }] });
8218
+ return true;
8219
+ }
8220
+ async function finishOrReflect(inputs, ctx, llmOutput) {
8221
+ if (shouldNudgeAndContinue(ctx, llmOutput)) return "continue";
8222
+ if (await reflectAfterStop(inputs, ctx)) return "continue";
8223
+ ctx.finalStatus = "finished";
8224
+ return "done";
8225
+ }
8158
8226
  async function runIteration(inputs, ctx) {
8159
8227
  const llmOutput = await streamLlmTurn(inputs, ctx);
8160
8228
  accumulateUsage(ctx.usage, llmOutput);
@@ -8188,9 +8256,7 @@ async function continueOrTerminate(inputs, ctx, llmOutput) {
8188
8256
  await emitAssistantTextStep(inputs, ctx, llmOutput.text);
8189
8257
  }
8190
8258
  if (llmOutput.stopReason !== "tool_use" || llmOutput.toolCalls.length === 0) {
8191
- if (shouldNudgeAndContinue(ctx, llmOutput)) return "continue";
8192
- ctx.finalStatus = "finished";
8193
- return "done";
8259
+ return finishOrReflect(inputs, ctx, llmOutput);
8194
8260
  }
8195
8261
  ctx.messages.push(buildAssistantTurn(llmOutput.text, llmOutput.toolCalls));
8196
8262
  const toolResults = await dispatchTools(inputs, ctx.tools, llmOutput.toolCalls, ctx.events);
@@ -8213,7 +8279,7 @@ async function continueOrTerminate(inputs, ctx, llmOutput) {
8213
8279
  pushToolConversationSteps(ctx, llmOutput.toolCalls, toolResults);
8214
8280
  return handleToolErrorContinuation(inputs, ctx, toolResults);
8215
8281
  }
8216
- var MAX_NUDGE_ATTEMPTS;
8282
+ var MAX_NUDGE_ATTEMPTS, MAX_STOP_FEEDBACK_ATTEMPTS;
8217
8283
  var init_loop = __esm({
8218
8284
  "src/internal/agent-loop/loop.ts"() {
8219
8285
  init_budget();
@@ -8226,6 +8292,7 @@ var init_loop = __esm({
8226
8292
  init_tool_dispatch();
8227
8293
  init_usage_and_cost();
8228
8294
  MAX_NUDGE_ATTEMPTS = 2;
8295
+ MAX_STOP_FEEDBACK_ATTEMPTS = 2;
8229
8296
  }
8230
8297
  });
8231
8298
 
@@ -14190,6 +14257,71 @@ var init_agent_factory_registry = __esm({
14190
14257
  }
14191
14258
  });
14192
14259
 
14260
+ // src/internal/runtime/lifecycle/run-to-completion.ts
14261
+ var run_to_completion_exports = {};
14262
+ __export(run_to_completion_exports, {
14263
+ classifyRound: () => classifyRound,
14264
+ runToCompletionImpl: () => runToCompletionImpl
14265
+ });
14266
+ function isEmptyRound(result) {
14267
+ return (result.result ?? "").trim() === "";
14268
+ }
14269
+ function classifyRound(result, round, maxRounds, emptyStreak) {
14270
+ if (result.stoppedAtIterationLimit !== true) return "done";
14271
+ if (isEmptyRound(result) && emptyStreak >= 1) return "no_progress";
14272
+ if (round >= maxRounds) return "step_limit";
14273
+ return "continue";
14274
+ }
14275
+ function addUsage(acc, u) {
14276
+ if (u === void 0) return acc;
14277
+ const inputTokens = (acc?.inputTokens ?? 0) + u.inputTokens;
14278
+ const outputTokens = (acc?.outputTokens ?? 0) + u.outputTokens;
14279
+ const sumOpt = (a, b) => a === void 0 && b === void 0 ? void 0 : (a ?? 0) + (b ?? 0);
14280
+ return {
14281
+ inputTokens,
14282
+ outputTokens,
14283
+ totalTokens: inputTokens + outputTokens,
14284
+ cacheReadTokens: sumOpt(acc?.cacheReadTokens, u.cacheReadTokens),
14285
+ cacheWriteTokens: sumOpt(acc?.cacheWriteTokens, u.cacheWriteTokens),
14286
+ reasoningTokens: sumOpt(acc?.reasoningTokens, u.reasoningTokens)
14287
+ };
14288
+ }
14289
+ function buildResult(terminal, rounds, lastResult, usage) {
14290
+ return { terminal, rounds, lastResult, ...usage !== void 0 ? { usage } : {} };
14291
+ }
14292
+ async function stepRound(agent, prompt, sendOptions, round, maxRounds, state2) {
14293
+ const run = await agent.send(prompt, sendOptions);
14294
+ const result = await run.wait();
14295
+ const usage = addUsage(state2.usage, result.usage);
14296
+ const decision = classifyRound(result, round, maxRounds, state2.emptyStreak);
14297
+ if (decision !== "continue") return { terminal: buildResult(decision, round, result, usage) };
14298
+ const emptyStreak = isEmptyRound(result) ? state2.emptyStreak + 1 : 0;
14299
+ return { next: { usage, emptyStreak }, lastResult: result };
14300
+ }
14301
+ async function runToCompletionImpl(agent, message, options) {
14302
+ const maxRounds = options?.maxRounds ?? DEFAULT_MAX_ROUNDS;
14303
+ const continuationPrompt = options?.continuationPrompt ?? DEFAULT_CONTINUATION_PROMPT;
14304
+ const { onTruncated, signal, sendOptions } = options ?? {};
14305
+ let state2 = { usage: void 0, emptyStreak: 0 };
14306
+ for (let round = 0; ; round += 1) {
14307
+ const prompt = round === 0 ? message : continuationPrompt;
14308
+ const outcome = await stepRound(agent, prompt, sendOptions, round, maxRounds, state2);
14309
+ if ("terminal" in outcome) return outcome.terminal;
14310
+ state2 = outcome.next;
14311
+ await onTruncated?.({ round });
14312
+ if (signal?.aborted === true) {
14313
+ return buildResult("step_limit", round, outcome.lastResult, state2.usage);
14314
+ }
14315
+ }
14316
+ }
14317
+ var DEFAULT_MAX_ROUNDS, DEFAULT_CONTINUATION_PROMPT;
14318
+ var init_run_to_completion = __esm({
14319
+ "src/internal/runtime/lifecycle/run-to-completion.ts"() {
14320
+ DEFAULT_MAX_ROUNDS = 5;
14321
+ DEFAULT_CONTINUATION_PROMPT = "Continue from where you left off and finish the task. If it is already complete, give the final answer.";
14322
+ }
14323
+ });
14324
+
14193
14325
  // src/internal/runtime/lifecycle/fork-agent.ts
14194
14326
  var fork_agent_exports = {};
14195
14327
  __export(fork_agent_exports, {
@@ -14270,6 +14402,13 @@ function localAgentRunUntil(agent, goal, options) {
14270
14402
  }
14271
14403
  return wrap();
14272
14404
  }
14405
+ function localAgentRunToCompletion(agent, message, options) {
14406
+ async function run() {
14407
+ const { runToCompletionImpl: runToCompletionImpl2 } = await Promise.resolve().then(() => (init_run_to_completion(), run_to_completion_exports));
14408
+ return runToCompletionImpl2({ send: (m, o) => agent.send(m, o) }, message, options);
14409
+ }
14410
+ return run();
14411
+ }
14273
14412
  async function localAgentFork(parent, options) {
14274
14413
  const { forkAgentImpl: forkAgentImpl2 } = await Promise.resolve().then(() => (init_fork_agent(), fork_agent_exports));
14275
14414
  const { getAgentFacade: getAgentFacade2 } = await Promise.resolve().then(() => (init_agent_factory_registry(), agent_factory_registry_exports));
@@ -15404,6 +15543,10 @@ var init_local_agent = __esm({
15404
15543
  fork(options) {
15405
15544
  return localAgentFork({ agentId: this.agentId, options: this.options, personalitySlugSnapshot: this.personalityStore.active(this.agentId) }, options);
15406
15545
  }
15546
+ // biome-ignore format: G8 budget — see runUntil comment above.
15547
+ runToCompletion(message, options) {
15548
+ return localAgentRunToCompletion(this, message, options);
15549
+ }
15407
15550
  };
15408
15551
  }
15409
15552
  });