@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.
- package/CHANGELOG.md +151 -0
- package/dist/cli/args.d.ts +16 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +80 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/bin.js +30 -66
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/consent.js +1 -1
- package/dist/cli/settings.d.ts.map +1 -1
- package/dist/cli/settings.js +33 -1
- package/dist/cli/settings.js.map +1 -1
- package/dist/core/adapters/adapter.d.ts +2 -2
- package/dist/core/adapters/claude-code.d.ts +60 -2
- package/dist/core/adapters/claude-code.d.ts.map +1 -1
- package/dist/core/adapters/claude-code.js +119 -32
- package/dist/core/adapters/claude-code.js.map +1 -1
- package/dist/core/adapters/codex.d.ts +21 -2
- package/dist/core/adapters/codex.d.ts.map +1 -1
- package/dist/core/adapters/codex.js +53 -2
- package/dist/core/adapters/codex.js.map +1 -1
- package/dist/core/adapters/detect.d.ts +9 -0
- package/dist/core/adapters/detect.d.ts.map +1 -1
- package/dist/core/adapters/detect.js +9 -0
- package/dist/core/adapters/detect.js.map +1 -1
- package/dist/core/api-key-provider.d.ts +79 -0
- package/dist/core/api-key-provider.d.ts.map +1 -0
- package/dist/core/api-key-provider.js +91 -0
- package/dist/core/api-key-provider.js.map +1 -0
- package/dist/core/canvas-body.d.ts +28 -28
- package/dist/core/canvas-body.d.ts.map +1 -1
- package/dist/core/canvas-body.js +117 -55
- package/dist/core/canvas-body.js.map +1 -1
- package/dist/core/canvas-derive.d.ts +43 -0
- package/dist/core/canvas-derive.d.ts.map +1 -0
- package/dist/core/canvas-derive.js +95 -0
- package/dist/core/canvas-derive.js.map +1 -0
- package/dist/core/canvas-enrichment.d.ts +52 -127
- package/dist/core/canvas-enrichment.d.ts.map +1 -1
- package/dist/core/canvas-enrichment.js +17 -178
- package/dist/core/canvas-enrichment.js.map +1 -1
- package/dist/core/canvas-graph.d.ts +40 -1
- package/dist/core/canvas-graph.d.ts.map +1 -1
- package/dist/core/canvas-graph.js +71 -9
- package/dist/core/canvas-graph.js.map +1 -1
- package/dist/core/canvas-interconnections.d.ts +26 -8
- package/dist/core/canvas-interconnections.d.ts.map +1 -1
- package/dist/core/canvas-interconnections.js +132 -24
- package/dist/core/canvas-interconnections.js.map +1 -1
- package/dist/core/canvas-manifest-check.d.ts.map +1 -1
- package/dist/core/canvas-manifest-check.js +15 -2
- package/dist/core/canvas-manifest-check.js.map +1 -1
- package/dist/core/canvas-render.d.ts +8 -15
- package/dist/core/canvas-render.d.ts.map +1 -1
- package/dist/core/canvas-render.js +29 -41
- package/dist/core/canvas-render.js.map +1 -1
- package/dist/core/canvas-run-state.d.ts +50 -5
- package/dist/core/canvas-run-state.d.ts.map +1 -1
- package/dist/core/canvas-run-state.js +176 -11
- package/dist/core/canvas-run-state.js.map +1 -1
- package/dist/core/canvas-svg.d.ts +9 -4
- package/dist/core/canvas-svg.d.ts.map +1 -1
- package/dist/core/canvas-svg.js +84 -13
- package/dist/core/canvas-svg.js.map +1 -1
- package/dist/core/canvas-template.d.ts +9 -10
- package/dist/core/canvas-template.d.ts.map +1 -1
- package/dist/core/canvas-template.js +57 -20
- package/dist/core/canvas-template.js.map +1 -1
- package/dist/core/canvas-watcher.d.ts +22 -1
- package/dist/core/canvas-watcher.d.ts.map +1 -1
- package/dist/core/canvas-watcher.js +111 -30
- package/dist/core/canvas-watcher.js.map +1 -1
- package/dist/core/collector/normalizer.d.ts +8 -0
- package/dist/core/collector/normalizer.d.ts.map +1 -1
- package/dist/core/collector/normalizer.js +8 -0
- package/dist/core/collector/normalizer.js.map +1 -1
- package/dist/core/collector/store.d.ts +75 -1
- package/dist/core/collector/store.d.ts.map +1 -1
- package/dist/core/collector/store.js +334 -2
- package/dist/core/collector/store.js.map +1 -1
- package/dist/core/definition-slug-resolver.d.ts +13 -0
- package/dist/core/definition-slug-resolver.d.ts.map +1 -1
- package/dist/core/definition-slug-resolver.js +29 -0
- package/dist/core/definition-slug-resolver.js.map +1 -1
- package/dist/core/errors.d.ts +10 -4
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +11 -5
- package/dist/core/errors.js.map +1 -1
- package/dist/core/example-seed.d.ts +8 -0
- package/dist/core/example-seed.d.ts.map +1 -1
- package/dist/core/example-seed.js +47 -10
- package/dist/core/example-seed.js.map +1 -1
- package/dist/core/inject/system-prompt.d.ts +8 -0
- package/dist/core/inject/system-prompt.d.ts.map +1 -1
- package/dist/core/inject/system-prompt.js +3 -1
- package/dist/core/inject/system-prompt.js.map +1 -1
- package/dist/core/macros.d.ts.map +1 -1
- package/dist/core/macros.js +5 -6
- package/dist/core/macros.js.map +1 -1
- package/dist/core/paths.d.ts +1 -0
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +1 -0
- package/dist/core/paths.js.map +1 -1
- package/dist/core/record-archive.d.ts +187 -0
- package/dist/core/record-archive.d.ts.map +1 -0
- package/dist/core/record-archive.js +465 -0
- package/dist/core/record-archive.js.map +1 -0
- package/dist/core/rehydration.d.ts +72 -0
- package/dist/core/rehydration.d.ts.map +1 -0
- package/dist/core/rehydration.js +72 -0
- package/dist/core/rehydration.js.map +1 -0
- package/dist/core/render-local-run.d.ts +82 -0
- package/dist/core/render-local-run.d.ts.map +1 -0
- package/dist/core/render-local-run.js +105 -0
- package/dist/core/render-local-run.js.map +1 -0
- package/dist/core/render-log-slice.d.ts +30 -0
- package/dist/core/render-log-slice.d.ts.map +1 -0
- package/dist/core/render-log-slice.js +49 -0
- package/dist/core/render-log-slice.js.map +1 -0
- package/dist/core/render-run-state.d.ts.map +1 -1
- package/dist/core/render-run-state.js +30 -49
- package/dist/core/render-run-state.js.map +1 -1
- package/dist/core/resume-brief.d.ts +130 -0
- package/dist/core/resume-brief.d.ts.map +1 -0
- package/dist/core/resume-brief.js +404 -0
- package/dist/core/resume-brief.js.map +1 -0
- package/dist/core/rolling-summary.d.ts +130 -0
- package/dist/core/rolling-summary.d.ts.map +1 -0
- package/dist/core/rolling-summary.js +227 -0
- package/dist/core/rolling-summary.js.map +1 -0
- package/dist/core/run-local-bootstrap.d.ts +93 -0
- package/dist/core/run-local-bootstrap.d.ts.map +1 -0
- package/dist/core/run-local-bootstrap.js +155 -0
- package/dist/core/run-local-bootstrap.js.map +1 -0
- package/dist/core/run-state.d.ts +16 -1
- package/dist/core/run-state.d.ts.map +1 -1
- package/dist/core/run-state.js +75 -25
- package/dist/core/run-state.js.map +1 -1
- package/dist/core/session-manager.d.ts +11 -3
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +56 -7
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-record.d.ts +165 -0
- package/dist/core/session-record.d.ts.map +1 -0
- package/dist/core/session-record.js +470 -0
- package/dist/core/session-record.js.map +1 -0
- package/dist/core/spawn-target.d.ts +12 -0
- package/dist/core/spawn-target.d.ts.map +1 -0
- package/dist/core/spawn-target.js +187 -0
- package/dist/core/spawn-target.js.map +1 -0
- package/dist/core/stub-feedback.d.ts +52 -0
- package/dist/core/stub-feedback.d.ts.map +1 -0
- package/dist/core/stub-feedback.js +33 -0
- package/dist/core/stub-feedback.js.map +1 -0
- package/dist/core/task-manager.d.ts.map +1 -1
- package/dist/core/task-manager.js +9 -1
- package/dist/core/task-manager.js.map +1 -1
- package/dist/core/template-catalog.d.ts +48 -0
- package/dist/core/template-catalog.d.ts.map +1 -0
- package/dist/core/template-catalog.js +338 -0
- package/dist/core/template-catalog.js.map +1 -0
- package/dist/core/workflow-registry.d.ts.map +1 -1
- package/dist/core/workflow-registry.js +2 -2
- package/dist/core/workflow-registry.js.map +1 -1
- package/dist/core/workspace-watcher.d.ts.map +1 -1
- package/dist/core/workspace-watcher.js +4 -3
- package/dist/core/workspace-watcher.js.map +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -1
- package/dist/profiles/default.d.ts.map +1 -1
- package/dist/profiles/default.js +9 -14
- package/dist/profiles/default.js.map +1 -1
- package/dist/server/actions.d.ts +139 -0
- package/dist/server/actions.d.ts.map +1 -0
- package/dist/server/actions.js +430 -0
- package/dist/server/actions.js.map +1 -0
- package/dist/server/auth-routes.d.ts +92 -0
- package/dist/server/auth-routes.d.ts.map +1 -0
- package/dist/server/auth-routes.js +151 -0
- package/dist/server/auth-routes.js.map +1 -0
- package/dist/server/canvas.d.ts.map +1 -1
- package/dist/server/canvas.js +7 -7
- package/dist/server/canvas.js.map +1 -1
- package/dist/server/fs.d.ts.map +1 -1
- package/dist/server/fs.js +17 -1
- package/dist/server/fs.js.map +1 -1
- package/dist/server/index.d.ts +12 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +0 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/ingest.d.ts +11 -0
- package/dist/server/ingest.d.ts.map +1 -1
- package/dist/server/ingest.js +1 -0
- package/dist/server/ingest.js.map +1 -1
- package/dist/server/macros.d.ts +4 -6
- package/dist/server/macros.d.ts.map +1 -1
- package/dist/server/macros.js.map +1 -1
- package/dist/server/rest.d.ts +18 -6
- package/dist/server/rest.d.ts.map +1 -1
- package/dist/server/rest.js +185 -50
- package/dist/server/rest.js.map +1 -1
- package/dist/server/runs.d.ts +16 -15
- package/dist/server/runs.d.ts.map +1 -1
- package/dist/server/runs.js +15 -80
- package/dist/server/runs.js.map +1 -1
- package/dist/server/static.d.ts +15 -1
- package/dist/server/static.d.ts.map +1 -1
- package/dist/server/static.js +54 -4
- package/dist/server/static.js.map +1 -1
- package/dist/server/templates.d.ts +24 -0
- package/dist/server/templates.d.ts.map +1 -0
- package/dist/server/templates.js +54 -0
- package/dist/server/templates.js.map +1 -0
- package/dist/shared/types.d.ts +558 -66
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +19 -11
- package/dist/shared/types.js.map +1 -1
- package/dist/web/assets/index-DC65cskq.js +446 -0
- package/dist/web/assets/index-xtD-3FFa.css +32 -0
- package/dist/web/canvas/sess-boot/index.html +305 -0
- package/dist/web/index.html +4 -9
- package/package.json +8 -4
- package/dist/core/canvas-enrich.d.ts +0 -115
- package/dist/core/canvas-enrich.d.ts.map +0 -1
- package/dist/core/canvas-enrich.js +0 -186
- package/dist/core/canvas-enrich.js.map +0 -1
- package/dist/core/run-spend.d.ts +0 -55
- package/dist/core/run-spend.d.ts.map +0 -1
- package/dist/core/run-spend.js +0 -109
- package/dist/core/run-spend.js.map +0 -1
- package/dist/core/run-transactions.d.ts +0 -60
- package/dist/core/run-transactions.d.ts.map +0 -1
- package/dist/core/run-transactions.js +0 -129
- package/dist/core/run-transactions.js.map +0 -1
- package/dist/profiles/canvas-guidelines.d.ts +0 -20
- package/dist/profiles/canvas-guidelines.d.ts.map +0 -1
- package/dist/profiles/canvas-guidelines.js +0 -48
- package/dist/profiles/canvas-guidelines.js.map +0 -1
- package/dist/server/skills.d.ts +0 -51
- package/dist/server/skills.d.ts.map +0 -1
- package/dist/server/skills.js +0 -312
- package/dist/server/skills.js.map +0 -1
- package/dist/web/assets/index-DM84J83Y.js +0 -234
- 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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"}
|
package/dist/core/run-state.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
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"}
|