@sapiom/harness 0.1.5 → 0.2.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 (245) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/dist/cli/args.d.ts +16 -0
  3. package/dist/cli/args.d.ts.map +1 -0
  4. package/dist/cli/args.js +80 -0
  5. package/dist/cli/args.js.map +1 -0
  6. package/dist/cli/bin.js +30 -66
  7. package/dist/cli/bin.js.map +1 -1
  8. package/dist/cli/consent.js +1 -1
  9. package/dist/cli/settings.d.ts.map +1 -1
  10. package/dist/cli/settings.js +33 -1
  11. package/dist/cli/settings.js.map +1 -1
  12. package/dist/core/adapters/adapter.d.ts +2 -2
  13. package/dist/core/adapters/claude-code.d.ts +60 -2
  14. package/dist/core/adapters/claude-code.d.ts.map +1 -1
  15. package/dist/core/adapters/claude-code.js +119 -32
  16. package/dist/core/adapters/claude-code.js.map +1 -1
  17. package/dist/core/adapters/codex.d.ts +21 -2
  18. package/dist/core/adapters/codex.d.ts.map +1 -1
  19. package/dist/core/adapters/codex.js +53 -2
  20. package/dist/core/adapters/codex.js.map +1 -1
  21. package/dist/core/adapters/detect.d.ts +9 -0
  22. package/dist/core/adapters/detect.d.ts.map +1 -1
  23. package/dist/core/adapters/detect.js +9 -0
  24. package/dist/core/adapters/detect.js.map +1 -1
  25. package/dist/core/api-key-provider.d.ts +79 -0
  26. package/dist/core/api-key-provider.d.ts.map +1 -0
  27. package/dist/core/api-key-provider.js +91 -0
  28. package/dist/core/api-key-provider.js.map +1 -0
  29. package/dist/core/canvas-body.d.ts +28 -28
  30. package/dist/core/canvas-body.d.ts.map +1 -1
  31. package/dist/core/canvas-body.js +117 -55
  32. package/dist/core/canvas-body.js.map +1 -1
  33. package/dist/core/canvas-derive.d.ts +43 -0
  34. package/dist/core/canvas-derive.d.ts.map +1 -0
  35. package/dist/core/canvas-derive.js +95 -0
  36. package/dist/core/canvas-derive.js.map +1 -0
  37. package/dist/core/canvas-enrichment.d.ts +52 -127
  38. package/dist/core/canvas-enrichment.d.ts.map +1 -1
  39. package/dist/core/canvas-enrichment.js +17 -178
  40. package/dist/core/canvas-enrichment.js.map +1 -1
  41. package/dist/core/canvas-graph.d.ts +40 -1
  42. package/dist/core/canvas-graph.d.ts.map +1 -1
  43. package/dist/core/canvas-graph.js +71 -9
  44. package/dist/core/canvas-graph.js.map +1 -1
  45. package/dist/core/canvas-interconnections.d.ts +26 -8
  46. package/dist/core/canvas-interconnections.d.ts.map +1 -1
  47. package/dist/core/canvas-interconnections.js +132 -24
  48. package/dist/core/canvas-interconnections.js.map +1 -1
  49. package/dist/core/canvas-manifest-check.d.ts.map +1 -1
  50. package/dist/core/canvas-manifest-check.js +15 -2
  51. package/dist/core/canvas-manifest-check.js.map +1 -1
  52. package/dist/core/canvas-render.d.ts +8 -15
  53. package/dist/core/canvas-render.d.ts.map +1 -1
  54. package/dist/core/canvas-render.js +29 -41
  55. package/dist/core/canvas-render.js.map +1 -1
  56. package/dist/core/canvas-run-state.d.ts +50 -5
  57. package/dist/core/canvas-run-state.d.ts.map +1 -1
  58. package/dist/core/canvas-run-state.js +176 -11
  59. package/dist/core/canvas-run-state.js.map +1 -1
  60. package/dist/core/canvas-svg.d.ts +9 -4
  61. package/dist/core/canvas-svg.d.ts.map +1 -1
  62. package/dist/core/canvas-svg.js +84 -13
  63. package/dist/core/canvas-svg.js.map +1 -1
  64. package/dist/core/canvas-template.d.ts +9 -10
  65. package/dist/core/canvas-template.d.ts.map +1 -1
  66. package/dist/core/canvas-template.js +57 -20
  67. package/dist/core/canvas-template.js.map +1 -1
  68. package/dist/core/canvas-watcher.d.ts +22 -1
  69. package/dist/core/canvas-watcher.d.ts.map +1 -1
  70. package/dist/core/canvas-watcher.js +111 -30
  71. package/dist/core/canvas-watcher.js.map +1 -1
  72. package/dist/core/collector/normalizer.d.ts +8 -0
  73. package/dist/core/collector/normalizer.d.ts.map +1 -1
  74. package/dist/core/collector/normalizer.js +8 -0
  75. package/dist/core/collector/normalizer.js.map +1 -1
  76. package/dist/core/collector/store.d.ts +75 -1
  77. package/dist/core/collector/store.d.ts.map +1 -1
  78. package/dist/core/collector/store.js +334 -2
  79. package/dist/core/collector/store.js.map +1 -1
  80. package/dist/core/definition-slug-resolver.d.ts +13 -0
  81. package/dist/core/definition-slug-resolver.d.ts.map +1 -1
  82. package/dist/core/definition-slug-resolver.js +29 -0
  83. package/dist/core/definition-slug-resolver.js.map +1 -1
  84. package/dist/core/errors.d.ts +10 -4
  85. package/dist/core/errors.d.ts.map +1 -1
  86. package/dist/core/errors.js +11 -5
  87. package/dist/core/errors.js.map +1 -1
  88. package/dist/core/example-seed.d.ts +8 -0
  89. package/dist/core/example-seed.d.ts.map +1 -1
  90. package/dist/core/example-seed.js +47 -10
  91. package/dist/core/example-seed.js.map +1 -1
  92. package/dist/core/inject/system-prompt.d.ts +8 -0
  93. package/dist/core/inject/system-prompt.d.ts.map +1 -1
  94. package/dist/core/inject/system-prompt.js +3 -1
  95. package/dist/core/inject/system-prompt.js.map +1 -1
  96. package/dist/core/macros.d.ts.map +1 -1
  97. package/dist/core/macros.js +5 -6
  98. package/dist/core/macros.js.map +1 -1
  99. package/dist/core/paths.d.ts +1 -0
  100. package/dist/core/paths.d.ts.map +1 -1
  101. package/dist/core/paths.js +1 -0
  102. package/dist/core/paths.js.map +1 -1
  103. package/dist/core/record-archive.d.ts +187 -0
  104. package/dist/core/record-archive.d.ts.map +1 -0
  105. package/dist/core/record-archive.js +465 -0
  106. package/dist/core/record-archive.js.map +1 -0
  107. package/dist/core/rehydration.d.ts +72 -0
  108. package/dist/core/rehydration.d.ts.map +1 -0
  109. package/dist/core/rehydration.js +72 -0
  110. package/dist/core/rehydration.js.map +1 -0
  111. package/dist/core/render-local-run.d.ts +82 -0
  112. package/dist/core/render-local-run.d.ts.map +1 -0
  113. package/dist/core/render-local-run.js +105 -0
  114. package/dist/core/render-local-run.js.map +1 -0
  115. package/dist/core/render-log-slice.d.ts +30 -0
  116. package/dist/core/render-log-slice.d.ts.map +1 -0
  117. package/dist/core/render-log-slice.js +49 -0
  118. package/dist/core/render-log-slice.js.map +1 -0
  119. package/dist/core/render-run-state.d.ts.map +1 -1
  120. package/dist/core/render-run-state.js +30 -49
  121. package/dist/core/render-run-state.js.map +1 -1
  122. package/dist/core/resume-brief.d.ts +130 -0
  123. package/dist/core/resume-brief.d.ts.map +1 -0
  124. package/dist/core/resume-brief.js +404 -0
  125. package/dist/core/resume-brief.js.map +1 -0
  126. package/dist/core/rolling-summary.d.ts +130 -0
  127. package/dist/core/rolling-summary.d.ts.map +1 -0
  128. package/dist/core/rolling-summary.js +227 -0
  129. package/dist/core/rolling-summary.js.map +1 -0
  130. package/dist/core/run-local-bootstrap.d.ts +93 -0
  131. package/dist/core/run-local-bootstrap.d.ts.map +1 -0
  132. package/dist/core/run-local-bootstrap.js +155 -0
  133. package/dist/core/run-local-bootstrap.js.map +1 -0
  134. package/dist/core/run-state.d.ts +16 -1
  135. package/dist/core/run-state.d.ts.map +1 -1
  136. package/dist/core/run-state.js +75 -25
  137. package/dist/core/run-state.js.map +1 -1
  138. package/dist/core/session-manager.d.ts +11 -3
  139. package/dist/core/session-manager.d.ts.map +1 -1
  140. package/dist/core/session-manager.js +56 -7
  141. package/dist/core/session-manager.js.map +1 -1
  142. package/dist/core/session-record.d.ts +165 -0
  143. package/dist/core/session-record.d.ts.map +1 -0
  144. package/dist/core/session-record.js +470 -0
  145. package/dist/core/session-record.js.map +1 -0
  146. package/dist/core/spawn-target.d.ts +12 -0
  147. package/dist/core/spawn-target.d.ts.map +1 -0
  148. package/dist/core/spawn-target.js +187 -0
  149. package/dist/core/spawn-target.js.map +1 -0
  150. package/dist/core/stub-feedback.d.ts +52 -0
  151. package/dist/core/stub-feedback.d.ts.map +1 -0
  152. package/dist/core/stub-feedback.js +33 -0
  153. package/dist/core/stub-feedback.js.map +1 -0
  154. package/dist/core/task-manager.d.ts.map +1 -1
  155. package/dist/core/task-manager.js +9 -1
  156. package/dist/core/task-manager.js.map +1 -1
  157. package/dist/core/template-catalog.d.ts +48 -0
  158. package/dist/core/template-catalog.d.ts.map +1 -0
  159. package/dist/core/template-catalog.js +338 -0
  160. package/dist/core/template-catalog.js.map +1 -0
  161. package/dist/core/workflow-registry.d.ts.map +1 -1
  162. package/dist/core/workflow-registry.js +2 -2
  163. package/dist/core/workflow-registry.js.map +1 -1
  164. package/dist/core/workspace-watcher.d.ts.map +1 -1
  165. package/dist/core/workspace-watcher.js +4 -3
  166. package/dist/core/workspace-watcher.js.map +1 -1
  167. package/dist/index.d.ts +12 -0
  168. package/dist/index.d.ts.map +1 -1
  169. package/dist/index.js +21 -0
  170. package/dist/index.js.map +1 -1
  171. package/dist/profiles/default.d.ts.map +1 -1
  172. package/dist/profiles/default.js +9 -14
  173. package/dist/profiles/default.js.map +1 -1
  174. package/dist/server/actions.d.ts +139 -0
  175. package/dist/server/actions.d.ts.map +1 -0
  176. package/dist/server/actions.js +430 -0
  177. package/dist/server/actions.js.map +1 -0
  178. package/dist/server/auth-routes.d.ts +92 -0
  179. package/dist/server/auth-routes.d.ts.map +1 -0
  180. package/dist/server/auth-routes.js +151 -0
  181. package/dist/server/auth-routes.js.map +1 -0
  182. package/dist/server/canvas.d.ts.map +1 -1
  183. package/dist/server/canvas.js +7 -7
  184. package/dist/server/canvas.js.map +1 -1
  185. package/dist/server/fs.d.ts.map +1 -1
  186. package/dist/server/fs.js +17 -1
  187. package/dist/server/fs.js.map +1 -1
  188. package/dist/server/index.d.ts +12 -3
  189. package/dist/server/index.d.ts.map +1 -1
  190. package/dist/server/index.js +0 -0
  191. package/dist/server/index.js.map +1 -1
  192. package/dist/server/ingest.d.ts +11 -0
  193. package/dist/server/ingest.d.ts.map +1 -1
  194. package/dist/server/ingest.js +1 -0
  195. package/dist/server/ingest.js.map +1 -1
  196. package/dist/server/macros.d.ts +4 -6
  197. package/dist/server/macros.d.ts.map +1 -1
  198. package/dist/server/macros.js.map +1 -1
  199. package/dist/server/rest.d.ts +18 -6
  200. package/dist/server/rest.d.ts.map +1 -1
  201. package/dist/server/rest.js +185 -50
  202. package/dist/server/rest.js.map +1 -1
  203. package/dist/server/runs.d.ts +16 -15
  204. package/dist/server/runs.d.ts.map +1 -1
  205. package/dist/server/runs.js +15 -80
  206. package/dist/server/runs.js.map +1 -1
  207. package/dist/server/static.d.ts +15 -1
  208. package/dist/server/static.d.ts.map +1 -1
  209. package/dist/server/static.js +54 -4
  210. package/dist/server/static.js.map +1 -1
  211. package/dist/server/templates.d.ts +24 -0
  212. package/dist/server/templates.d.ts.map +1 -0
  213. package/dist/server/templates.js +54 -0
  214. package/dist/server/templates.js.map +1 -0
  215. package/dist/shared/types.d.ts +558 -66
  216. package/dist/shared/types.d.ts.map +1 -1
  217. package/dist/shared/types.js +19 -11
  218. package/dist/shared/types.js.map +1 -1
  219. package/dist/web/assets/index-DC65cskq.js +446 -0
  220. package/dist/web/assets/index-xtD-3FFa.css +32 -0
  221. package/dist/web/canvas/sess-boot/index.html +305 -0
  222. package/dist/web/index.html +4 -9
  223. package/package.json +8 -4
  224. package/dist/core/canvas-enrich.d.ts +0 -115
  225. package/dist/core/canvas-enrich.d.ts.map +0 -1
  226. package/dist/core/canvas-enrich.js +0 -186
  227. package/dist/core/canvas-enrich.js.map +0 -1
  228. package/dist/core/run-spend.d.ts +0 -55
  229. package/dist/core/run-spend.d.ts.map +0 -1
  230. package/dist/core/run-spend.js +0 -109
  231. package/dist/core/run-spend.js.map +0 -1
  232. package/dist/core/run-transactions.d.ts +0 -60
  233. package/dist/core/run-transactions.d.ts.map +0 -1
  234. package/dist/core/run-transactions.js +0 -129
  235. package/dist/core/run-transactions.js.map +0 -1
  236. package/dist/profiles/canvas-guidelines.d.ts +0 -20
  237. package/dist/profiles/canvas-guidelines.d.ts.map +0 -1
  238. package/dist/profiles/canvas-guidelines.js +0 -48
  239. package/dist/profiles/canvas-guidelines.js.map +0 -1
  240. package/dist/server/skills.d.ts +0 -51
  241. package/dist/server/skills.d.ts.map +0 -1
  242. package/dist/server/skills.js +0 -312
  243. package/dist/server/skills.js.map +0 -1
  244. package/dist/web/assets/index-DM84J83Y.js +0 -234
  245. package/dist/web/assets/index-DemtWW4L.css +0 -32
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Rolling session summary — the thing that makes rehydration good rather than
3
+ * merely possible.
4
+ *
5
+ * A resume brief built from the last N turns alone tells a fresh session what
6
+ * just happened; it cannot tell it what the session was *for*. So, opt-in, we
7
+ * periodically fold the whole record down to a ≤500-word summary and cache it
8
+ * at `<generated>/<harnessSessionId>/summary.md`. The brief
9
+ * (core/resume-brief.ts) picks it up when it's there and degrades cleanly to
10
+ * last-N-turns when it isn't — which is the default, since this is
11
+ * setting-gated (`HarnessSettings.rollingSummary`).
12
+ *
13
+ * NEVER BLOCKS A TURN. Everything here is fire-and-forget off the ingest path:
14
+ * `noteEvent` returns synchronously, the fold runs as a headless one-shot
15
+ * `TaskManager` job (a separate process, a cheap model, `--max-turns 1` — the
16
+ * bounded shape `HarnessAdapter.launchTask` exists for), and every failure
17
+ * mode short-circuits to "no summary". A user's session must never be slower,
18
+ * or fail, because a summary didn't happen.
19
+ *
20
+ * The task does not write the file itself. It is asked for the summary text
21
+ * and nothing else; we write `summary.md` from its `resultText` when it
22
+ * completes. Two reasons: the target lives under `<generated>/`, outside the
23
+ * session's cwd — which codex's `workspace-write` sandbox forbids and
24
+ * claude-code's `acceptEdits` shouldn't be asked to reach — and a task that
25
+ * writes its own output can half-write it, where a task that returns text
26
+ * either produces a whole summary or none.
27
+ *
28
+ * WHERE IT DOESN'T WORK: `launchTask` is optional on the adapter contract and
29
+ * codex has no non-interactive mode wired for it, so a codex session simply
30
+ * never produces a summary and its briefs are last-N-turns. That is a real
31
+ * gap, stated rather than papered over; the brief is honest either way.
32
+ */
33
+ import * as fs from "node:fs/promises";
34
+ import * as path from "node:path";
35
+ import { buildResumeBrief } from "./resume-brief.js";
36
+ /** Filename under `<generated>/<harnessSessionId>/`. */
37
+ export const ROLLING_SUMMARY_FILE = "summary.md";
38
+ /** Completed turns that must accumulate before a re-fold is worth its cost. */
39
+ export const ROLLING_SUMMARY_TURN_INTERVAL = 10;
40
+ /** The contract stated in the prompt, and the clamp applied to what comes back. */
41
+ export const ROLLING_SUMMARY_MAX_WORDS = 500;
42
+ /**
43
+ * Model alias for the fold. A summary of text we already hold is the cheapest
44
+ * possible LLM job — pinning a small model here keeps an opt-in background
45
+ * feature from quietly costing what the user's own session does.
46
+ */
47
+ export const ROLLING_SUMMARY_MODEL = "haiku";
48
+ /** Hard turn cap: one turn in, one summary out. */
49
+ export const ROLLING_SUMMARY_MAX_TURNS = 1;
50
+ /** Macro id the task is registered under — also TaskManager's dedupe key, so
51
+ * a session can never have two folds in flight at once. */
52
+ export const ROLLING_SUMMARY_MACRO_ID = "rolling-summary";
53
+ /**
54
+ * Token budget for the material handed to the fold. Larger than a brief's
55
+ * (this is throwaway input to a cheap model, not a system prompt competing
56
+ * with the user's own context) but still bounded, so a marathon session can't
57
+ * turn one fold into a huge request.
58
+ */
59
+ export const ROLLING_SUMMARY_INPUT_MAX_TOKENS = 20000;
60
+ /** Absolute path of a session's cached summary. */
61
+ export function rollingSummaryPath(generatedRoot, harnessSessionId) {
62
+ return path.join(generatedRoot, harnessSessionId, ROLLING_SUMMARY_FILE);
63
+ }
64
+ /**
65
+ * The cached summary for a session, or null when there is none — never throws.
66
+ * A brief that fails to find a summary is a brief without one, not an error.
67
+ */
68
+ export async function readRollingSummary(generatedRoot, harnessSessionId) {
69
+ try {
70
+ const text = await fs.readFile(rollingSummaryPath(generatedRoot, harnessSessionId), "utf8");
71
+ const trimmed = text.trim();
72
+ return trimmed.length > 0 ? trimmed : null;
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
78
+ /** Trim `text` to at most `maxWords` words, marking the cut. */
79
+ export function clampToWords(text, maxWords = ROLLING_SUMMARY_MAX_WORDS) {
80
+ const words = text.trim().split(/\s+/);
81
+ if (words.length <= maxWords)
82
+ return text.trim();
83
+ return `${words.slice(0, maxWords).join(" ")} …[summary truncated]`;
84
+ }
85
+ /**
86
+ * The one-shot prompt handed to `launchTask`. The material is the record
87
+ * rendered by {@link buildResumeBrief} at a larger budget — deliberately the
88
+ * same renderer the brief uses, so the fold reads exactly the shape a future
89
+ * brief will present, and there is one place where "what a record looks like
90
+ * as text" is defined.
91
+ *
92
+ * `previous` is folded back in rather than discarded: the record itself is
93
+ * capped at the input budget, so on a long session the earliest turns are
94
+ * already gone from the material, and the last summary is the only surviving
95
+ * account of them.
96
+ */
97
+ export function buildRollingSummaryPrompt(record, previous) {
98
+ const material = buildResumeBrief(record, {
99
+ summary: previous,
100
+ maxTokens: ROLLING_SUMMARY_INPUT_MAX_TOKENS,
101
+ maxTurns: Number.MAX_SAFE_INTEGER,
102
+ });
103
+ return [
104
+ `Summarize the coding session described below in at most ${ROLLING_SUMMARY_MAX_WORDS} words.`,
105
+ "",
106
+ "The summary is read by a FRESH agent session that has none of this context,",
107
+ "so write for someone picking the work up cold. Cover, in this order and only",
108
+ "where the material actually supports it:",
109
+ "",
110
+ "1. What the session was trying to accomplish.",
111
+ "2. What was decided, and why — decisions are the part a transcript replay",
112
+ " cannot reconstruct.",
113
+ "3. What was actually changed (files, behaviour).",
114
+ "4. What is unfinished, blocked, or known-broken.",
115
+ "",
116
+ "Rules:",
117
+ "- Output ONLY the summary as markdown prose. No preamble, no sign-off, no",
118
+ " code fences around the whole thing.",
119
+ "- Do not invent anything the material does not state. If something is",
120
+ " unclear or was never recorded, say so or leave it out — a confident",
121
+ " summary of a gap is the single worst outcome here.",
122
+ "- Do not use any tools. Everything you need is in this prompt.",
123
+ "",
124
+ "--- SESSION MATERIAL ---",
125
+ "",
126
+ material,
127
+ ].join("\n");
128
+ }
129
+ export function createRollingSummarizer(deps) {
130
+ const turnInterval = deps.turnInterval ?? ROLLING_SUMMARY_TURN_INTERVAL;
131
+ const onError = deps.onError ?? ((err) => console.error("[harness] rolling summary failed:", err));
132
+ /** Completed turns seen for a session since its last fold was *started*. */
133
+ const turnsSinceFold = new Map();
134
+ /** Fold task id → the session it summarizes, so a completion can be routed
135
+ * back. Only ids this summarizer started are ever in here, which is what
136
+ * keeps it from writing summary.md for some other background task. */
137
+ const foldTargets = new Map();
138
+ /** In-flight promises, awaited by `idle()`. */
139
+ const pending = new Set();
140
+ const track = (work) => {
141
+ const tracked = work.catch(onError).finally(() => pending.delete(tracked));
142
+ pending.add(tracked);
143
+ };
144
+ async function fold(harnessSessionId) {
145
+ if (!(await deps.enabled()))
146
+ return;
147
+ const session = deps.getSession(harnessSessionId);
148
+ // Gone from the registry (swept, or a task's own id leaking through the
149
+ // ingest path) — there is nothing to run the fold as.
150
+ if (!session)
151
+ return;
152
+ const record = await deps.readRecord(harnessSessionId);
153
+ if (!record || record.turns.length === 0)
154
+ return;
155
+ const previous = await readRollingSummary(deps.generatedRoot, harnessSessionId);
156
+ const task = await deps.runTask({
157
+ macroId: ROLLING_SUMMARY_MACRO_ID,
158
+ label: "Session summary",
159
+ harnessSessionId,
160
+ harness: session.harness,
161
+ cwd: session.cwd,
162
+ prompt: buildRollingSummaryPrompt(record, previous),
163
+ model: ROLLING_SUMMARY_MODEL,
164
+ maxTurns: ROLLING_SUMMARY_MAX_TURNS,
165
+ });
166
+ foldTargets.set(task.id, harnessSessionId);
167
+ }
168
+ async function writeSummary(harnessSessionId, text) {
169
+ const clamped = clampToWords(text.trim());
170
+ if (clamped.length === 0)
171
+ return;
172
+ const filePath = rollingSummaryPath(deps.generatedRoot, harnessSessionId);
173
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
174
+ await fs.writeFile(filePath, `${clamped}\n`, "utf8");
175
+ }
176
+ return {
177
+ noteEvent(event) {
178
+ const sessionId = event.harnessSessionId;
179
+ if (event.type === "turn.completed") {
180
+ const count = (turnsSinceFold.get(sessionId) ?? 0) + 1;
181
+ if (count < turnInterval) {
182
+ turnsSinceFold.set(sessionId, count);
183
+ return;
184
+ }
185
+ // Reset BEFORE the fold rather than after it completes: the counter
186
+ // paces how often folds are *started*, and resetting on completion
187
+ // would let a slow fold queue up a second one the moment it lands.
188
+ turnsSinceFold.set(sessionId, 0);
189
+ track(fold(sessionId));
190
+ return;
191
+ }
192
+ if (event.type === "session.end") {
193
+ // Always fold at session end when anything at all happened since the
194
+ // last one — this is the summary a future rehydration will actually
195
+ // read, so it must not be `turnInterval - 1` turns stale.
196
+ const pendingTurns = turnsSinceFold.get(sessionId) ?? 0;
197
+ turnsSinceFold.delete(sessionId);
198
+ if (pendingTurns > 0)
199
+ track(fold(sessionId));
200
+ }
201
+ },
202
+ noteTaskStatus(task) {
203
+ const harnessSessionId = foldTargets.get(task.id);
204
+ if (harnessSessionId === undefined)
205
+ return;
206
+ if (task.status === "running")
207
+ return;
208
+ foldTargets.delete(task.id);
209
+ if (task.status === "failed" || !task.resultText) {
210
+ // A failed fold leaves the previous summary in place — a stale summary
211
+ // is strictly better than none, and the brief labels its own age via
212
+ // the record's own timestamps either way.
213
+ if (task.status === "failed")
214
+ onError(new Error(task.errorTail ?? "rolling summary task failed"));
215
+ return;
216
+ }
217
+ track(writeSummary(harnessSessionId, task.resultText));
218
+ },
219
+ async idle() {
220
+ // Re-read the set each pass: writing a summary is itself tracked work
221
+ // started by an earlier tracked promise.
222
+ while (pending.size > 0)
223
+ await Promise.all([...pending]);
224
+ },
225
+ };
226
+ }
227
+ //# sourceMappingURL=rolling-summary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rolling-summary.js","sourceRoot":"","sources":["../../src/core/rolling-summary.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * run-local bootstrap — the child-process entrypoint behind
3
+ * `POST /api/runs/local`.
4
+ *
5
+ * It runs an agent entirely in-process against stub capabilities
6
+ * (`runLocalFromDir` from @sapiom/agent-core) and writes the result as an
7
+ * NDJSON stream to stdout: one line per {@link LocalStepTrace}, then a single
8
+ * terminal summary line `{ outcome, output, error, unusedStubs, stubWarnings }`.
9
+ * Fully offline and zero-cost — run-local resolves every `ctx.sapiom.*` call
10
+ * from stubs and never touches the network.
11
+ *
12
+ * Why a separate child process (not an in-process import):
13
+ * 1. `runLocalFromDir` esbuild-bundles and dynamically `import()`s a workflow
14
+ * project the harness doesn't control, so a stray top-level side effect,
15
+ * infinite loop, or crash in someone else's step body is bounded to this
16
+ * one child instead of taking the long-lived harness server down.
17
+ * 2. It sidesteps a Vite/Vitest limitation: the dynamic `import(\`file://…\`)`
18
+ * inside the loader gets intercepted by the SSR dynamic-import-vars
19
+ * transform when pulled into a Vitest module graph, and mishandles the
20
+ * tmpdir `file://` URL on darwin. A plain child `node` process never goes
21
+ * through that transform. (Mirrors the reasoning in canvas-manifest-check.)
22
+ *
23
+ * Contract with the route ({@link createActionsRouter}):
24
+ * - Request arrives as one JSON object on **stdin**: `{ sourceDir, input?,
25
+ * stubs?, maxAttemptsPerStep? }`.
26
+ * - Output is line-oriented JSON on **stdout** — the route forwards each line
27
+ * through unchanged, so the shapes here ARE the wire shapes the SPA parses.
28
+ * - Diagnostics go to **stderr**; the route keeps a bounded tail for failures.
29
+ * - Exit 0 once the terminal line is written (even for a failed run — a failed
30
+ * *run* is a successful *invocation*); exit 1 only when no run happened
31
+ * (bad request, load error) after writing a terminal `error` line.
32
+ */
33
+ import { type LocalRunOutcome, type StubFile } from "@sapiom/agent-core";
34
+ /** The request shape the route writes to this child's stdin. */
35
+ export interface RunLocalRequest {
36
+ /** Absolute path to the agent project directory (contains `index.ts`). */
37
+ sourceDir: string;
38
+ /** The workflow's entry-step input (optional; agent-core defaults it). */
39
+ input?: unknown;
40
+ /** Explicit stub overrides. When omitted the project's committed
41
+ * `.sapiom-dev/stubs.json` is used (agent-core's own precedence). */
42
+ stubs?: StubFile;
43
+ /** Per-step attempt cap (optional; agent-core defaults it). */
44
+ maxAttemptsPerStep?: number;
45
+ }
46
+ /**
47
+ * The terminal NDJSON line, written after every per-step trace line. Carries
48
+ * the run-level outcome and the two stub-hygiene signals the inspector surfaces
49
+ * (WB15-2): `unusedStubs` (supplied keys that matched no capability call) and
50
+ * `stubWarnings` (keys that matched but carried the wrong shape).
51
+ *
52
+ * `kind: "summary"` discriminates it from a step-trace line so a consumer never
53
+ * has to guess which line is terminal.
54
+ */
55
+ export interface RunLocalSummaryLine {
56
+ kind: "summary";
57
+ outcome: LocalRunOutcome;
58
+ output?: unknown;
59
+ error?: unknown;
60
+ unusedStubs: Array<{
61
+ step: string;
62
+ key: string;
63
+ }>;
64
+ stubWarnings: string[];
65
+ }
66
+ /**
67
+ * The terminal line written when the run could not be *invoked* at all — a
68
+ * malformed request, an unreadable stub file, a project that fails to load.
69
+ * `outcome` is always `"failed"`; the child then exits non-zero. Distinct from
70
+ * a `summary` line, which represents a run that actually executed (even if that
71
+ * run's own outcome was `failed`).
72
+ */
73
+ export interface RunLocalErrorLine {
74
+ kind: "error";
75
+ outcome: "failed";
76
+ error: string;
77
+ }
78
+ /**
79
+ * Parse the stdin payload into a {@link RunLocalRequest}. Throws a plain Error
80
+ * with a caller-safe message (no key material, no provider names) on anything
81
+ * that isn't a JSON object carrying a non-empty string `sourceDir`.
82
+ */
83
+ export declare function parseRunLocalRequest(raw: string): RunLocalRequest;
84
+ /**
85
+ * Run the request and emit its NDJSON stream to `out`. Returns the process exit
86
+ * code: 0 when a run executed (any outcome), 1 when the run could not be
87
+ * invoked (a terminal `error` line is emitted first). Never throws — every
88
+ * failure becomes an in-band terminal line, because a half-written stream with
89
+ * a thrown stack on stderr is far harder for the route to reason about than a
90
+ * clean terminal line plus an exit code.
91
+ */
92
+ export declare function runBootstrap(request: RunLocalRequest, out: NodeJS.WritableStream): Promise<number>;
93
+ //# sourceMappingURL=run-local-bootstrap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-local-bootstrap.d.ts","sourceRoot":"","sources":["../../src/core/run-local-bootstrap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,QAAQ,EACd,MAAM,oBAAoB,CAAC;AAE5B,gEAAgE;AAChE,MAAM,WAAW,eAAe;IAC9B,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;0EACsE;IACtE,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,+DAA+D;IAC/D,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,eAAe,CAAC;IACzB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClD,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,QAAQ,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf;AAWD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAwBjE;AAYD;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,eAAe,EACxB,GAAG,EAAE,MAAM,CAAC,cAAc,GACzB,OAAO,CAAC,MAAM,CAAC,CAkCjB"}
@@ -0,0 +1,155 @@
1
+ /**
2
+ * run-local bootstrap — the child-process entrypoint behind
3
+ * `POST /api/runs/local`.
4
+ *
5
+ * It runs an agent entirely in-process against stub capabilities
6
+ * (`runLocalFromDir` from @sapiom/agent-core) and writes the result as an
7
+ * NDJSON stream to stdout: one line per {@link LocalStepTrace}, then a single
8
+ * terminal summary line `{ outcome, output, error, unusedStubs, stubWarnings }`.
9
+ * Fully offline and zero-cost — run-local resolves every `ctx.sapiom.*` call
10
+ * from stubs and never touches the network.
11
+ *
12
+ * Why a separate child process (not an in-process import):
13
+ * 1. `runLocalFromDir` esbuild-bundles and dynamically `import()`s a workflow
14
+ * project the harness doesn't control, so a stray top-level side effect,
15
+ * infinite loop, or crash in someone else's step body is bounded to this
16
+ * one child instead of taking the long-lived harness server down.
17
+ * 2. It sidesteps a Vite/Vitest limitation: the dynamic `import(\`file://…\`)`
18
+ * inside the loader gets intercepted by the SSR dynamic-import-vars
19
+ * transform when pulled into a Vitest module graph, and mishandles the
20
+ * tmpdir `file://` URL on darwin. A plain child `node` process never goes
21
+ * through that transform. (Mirrors the reasoning in canvas-manifest-check.)
22
+ *
23
+ * Contract with the route ({@link createActionsRouter}):
24
+ * - Request arrives as one JSON object on **stdin**: `{ sourceDir, input?,
25
+ * stubs?, maxAttemptsPerStep? }`.
26
+ * - Output is line-oriented JSON on **stdout** — the route forwards each line
27
+ * through unchanged, so the shapes here ARE the wire shapes the SPA parses.
28
+ * - Diagnostics go to **stderr**; the route keeps a bounded tail for failures.
29
+ * - Exit 0 once the terminal line is written (even for a failed run — a failed
30
+ * *run* is a successful *invocation*); exit 1 only when no run happened
31
+ * (bad request, load error) after writing a terminal `error` line.
32
+ */
33
+ import { runLocalFromDir, } from "@sapiom/agent-core";
34
+ /** Read the entire request payload from a readable stream as one UTF-8 string. */
35
+ async function readAll(stream) {
36
+ const chunks = [];
37
+ for await (const chunk of stream) {
38
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
39
+ }
40
+ return Buffer.concat(chunks).toString("utf8");
41
+ }
42
+ /**
43
+ * Parse the stdin payload into a {@link RunLocalRequest}. Throws a plain Error
44
+ * with a caller-safe message (no key material, no provider names) on anything
45
+ * that isn't a JSON object carrying a non-empty string `sourceDir`.
46
+ */
47
+ export function parseRunLocalRequest(raw) {
48
+ let parsed;
49
+ try {
50
+ parsed = JSON.parse(raw);
51
+ }
52
+ catch {
53
+ throw new Error("run-local request is not valid JSON");
54
+ }
55
+ if (typeof parsed !== "object" || parsed === null) {
56
+ throw new Error("run-local request must be a JSON object");
57
+ }
58
+ const body = parsed;
59
+ const sourceDir = body.sourceDir;
60
+ if (typeof sourceDir !== "string" || sourceDir.trim() === "") {
61
+ throw new Error("run-local request requires a non-empty sourceDir");
62
+ }
63
+ return {
64
+ sourceDir,
65
+ input: body.input,
66
+ stubs: body.stubs,
67
+ maxAttemptsPerStep: typeof body.maxAttemptsPerStep === "number"
68
+ ? body.maxAttemptsPerStep
69
+ : undefined,
70
+ };
71
+ }
72
+ /** Serialize one step trace as a single NDJSON line (newline-terminated). */
73
+ function traceLine(step) {
74
+ return JSON.stringify(step) + "\n";
75
+ }
76
+ /** Serialize the terminal summary as a single NDJSON line. */
77
+ function summaryLine(line) {
78
+ return JSON.stringify(line) + "\n";
79
+ }
80
+ /**
81
+ * Run the request and emit its NDJSON stream to `out`. Returns the process exit
82
+ * code: 0 when a run executed (any outcome), 1 when the run could not be
83
+ * invoked (a terminal `error` line is emitted first). Never throws — every
84
+ * failure becomes an in-band terminal line, because a half-written stream with
85
+ * a thrown stack on stderr is far harder for the route to reason about than a
86
+ * clean terminal line plus an exit code.
87
+ */
88
+ export async function runBootstrap(request, out) {
89
+ try {
90
+ const result = await runLocalFromDir({
91
+ sourceDir: request.sourceDir,
92
+ input: request.input,
93
+ stubs: request.stubs,
94
+ maxAttemptsPerStep: request.maxAttemptsPerStep,
95
+ });
96
+ // One line per step-attempt, in execution order — the consumer parses them
97
+ // incrementally rather than buffering the whole trace as a single blob.
98
+ for (const step of result.steps) {
99
+ out.write(traceLine(step));
100
+ }
101
+ const summary = {
102
+ kind: "summary",
103
+ outcome: result.outcome,
104
+ output: result.output,
105
+ error: result.error,
106
+ unusedStubs: result.unusedStubs,
107
+ stubWarnings: result.stubWarnings,
108
+ };
109
+ out.write(summaryLine(summary));
110
+ return 0;
111
+ }
112
+ catch (err) {
113
+ const errorLine = {
114
+ kind: "error",
115
+ outcome: "failed",
116
+ error: err instanceof Error ? err.message : String(err),
117
+ };
118
+ out.write(JSON.stringify(errorLine) + "\n");
119
+ return 1;
120
+ }
121
+ }
122
+ /**
123
+ * Entrypoint: read the request from stdin, run it, exit with the run's code.
124
+ * A stdin/read failure (or a request that can't be parsed) also degrades to a
125
+ * terminal `error` line + exit 1 — the route always sees a well-formed final
126
+ * line regardless of how the child failed.
127
+ */
128
+ async function main() {
129
+ let request;
130
+ try {
131
+ request = parseRunLocalRequest(await readAll(process.stdin));
132
+ }
133
+ catch (err) {
134
+ const errorLine = {
135
+ kind: "error",
136
+ outcome: "failed",
137
+ error: err instanceof Error ? err.message : String(err),
138
+ };
139
+ process.stdout.write(JSON.stringify(errorLine) + "\n");
140
+ process.exitCode = 1;
141
+ return;
142
+ }
143
+ process.exitCode = await runBootstrap(request, process.stdout);
144
+ }
145
+ // Only self-invoke when run as the child entrypoint, never when imported by a
146
+ // unit test (which drives runBootstrap / parseRunLocalRequest directly). The
147
+ // built entry is dist/core/run-local-bootstrap.js; import.meta.url ends with
148
+ // this module's own file, and process.argv[1] is the script node was told to
149
+ // run — they share a basename only for the real child launch.
150
+ const invokedAsScript = typeof process.argv[1] === "string" &&
151
+ import.meta.url === new URL(`file://${process.argv[1]}`).href;
152
+ if (invokedAsScript) {
153
+ void main();
154
+ }
155
+ //# sourceMappingURL=run-local-bootstrap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-local-bootstrap.js","sourceRoot":"","sources":["../../src/core/run-local-bootstrap.ts"],"names":[],"mappings":"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"}
@@ -16,6 +16,7 @@
16
16
  * elsewhere in the harness; consolidate into one shared helper when convenient.
17
17
  */
18
18
  import type { RunView } from "../shared/types.js";
19
+ import { type ApiKeyProvider } from "./api-key-provider.js";
19
20
  /** Resolve the agents surface base URL from the environment. */
20
21
  export declare function resolveAgentsBaseUrl(): string;
21
22
  export type RunStateResult = {
@@ -27,7 +28,12 @@ export type RunStateResult = {
27
28
  error: string;
28
29
  };
29
30
  export interface RunStateFetcherOpts {
30
- apiKey: string | null;
31
+ /**
32
+ * Sapiom API key. Accepts either a plain `string | null` (the boot-time key)
33
+ * or an {@link ApiKeyProvider} — pass a provider to get the refresh-on-401
34
+ * recovery path. A bare string never refreshes.
35
+ */
36
+ apiKey: string | null | ApiKeyProvider;
31
37
  baseUrl?: string;
32
38
  /** Injectable fetch implementation — defaults to global fetch. Test seam. */
33
39
  fetchImpl?: typeof fetch;
@@ -35,11 +41,20 @@ export interface RunStateFetcherOpts {
35
41
  export interface RunStateFetcher {
36
42
  fetch(executionId: string): Promise<RunStateResult>;
37
43
  }
44
+ /** Upstream statuses that mean "the API key was rejected" — worth one refresh
45
+ * + retry before we give up. 401 is the common case; 403 covers a key that
46
+ * authenticated but lost authorization (e.g. rotated to a new org). */
47
+ export declare function isAuthRejection(status: number): boolean;
38
48
  /**
39
49
  * Create a fetcher that resolves an execution's current render state from the
40
50
  * agents surface. The fetcher is intentionally non-throwing: all error paths
41
51
  * return a typed {@link RunStateResult} with `ok: false` so the router can
42
52
  * forward the appropriate HTTP status without a try/catch at the call site.
53
+ *
54
+ * When constructed with an {@link ApiKeyProvider}, a 401/403 from the upstream
55
+ * call triggers exactly one credential refresh + retry: if the shared store has
56
+ * a newer key (e.g. the user re-logged-in), the retry uses it and the Studio
57
+ * recovers in place instead of surfacing a dead-end auth error.
43
58
  */
44
59
  export declare function createRunStateFetcher(opts: RunStateFetcherOpts): RunStateFetcher;
45
60
  //# sourceMappingURL=run-state.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"run-state.d.ts","sourceRoot":"","sources":["../../src/core/run-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAGlD,gEAAgE;AAChE,wBAAgB,oBAAoB,IAAI,MAAM,CAM7C;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC9B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACrD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,mBAAmB,GACxB,eAAe,CA6CjB"}
1
+ {"version":3,"file":"run-state.d.ts","sourceRoot":"","sources":["../../src/core/run-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EACL,KAAK,cAAc,EAEpB,MAAM,uBAAuB,CAAC;AAgB/B,gEAAgE;AAChE,wBAAgB,oBAAoB,IAAI,MAAM,CAM7C;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC9B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjD,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,cAAc,CAAC;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACrD;AAED;;wEAEwE;AACxE,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,mBAAmB,GACxB,eAAe,CA4EjB"}