@tt-a1i/openpi 0.3.1 → 0.5.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/README.md +184 -59
- package/SETUP.md +23 -7
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- package/extensions/ask-user/index.ts +30 -14
- package/extensions/background-terminals/index.ts +30 -2
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +486 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +14 -6
- package/extensions/background-terminals/src/result-delivery.ts +4 -1
- package/extensions/background-terminals/src/ui/ps.ts +132 -129
- package/extensions/capabilities/index.ts +30 -42
- package/extensions/capabilities/src/ui.ts +93 -0
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/schedule.ts +7 -1
- package/extensions/file-mutation-display/index.ts +34 -76
- package/extensions/file-mutation-display/render.ts +146 -87
- package/extensions/file-search/index.ts +8 -7
- package/extensions/file-search/src/binaries.ts +75 -59
- package/extensions/git-info/src/changed-files-view.ts +47 -14
- package/extensions/git-read/index.ts +328 -0
- package/extensions/git-read/src/args.ts +171 -0
- package/extensions/git-read/src/process.ts +81 -0
- package/extensions/git-read/src/prompt.ts +56 -0
- package/extensions/model-info/index.ts +21 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +7 -2
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +222 -140
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +127 -131
- package/extensions/shared/activity-status.ts +36 -5
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/below-editor-navigation.ts +26 -0
- package/extensions/shared/capability-intent.ts +53 -0
- package/extensions/shared/child-session.ts +444 -22
- package/extensions/shared/result-budget.ts +134 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/screen-chrome.ts +133 -0
- package/extensions/shared/setup-config.ts +97 -38
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/spinner.ts +28 -0
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +113 -0
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +42 -8
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +461 -186
- package/extensions/subagents/navigation.ts +86 -28
- package/extensions/subagents/src/agent-types.ts +37 -15
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +375 -66
- package/extensions/subagents/src/domain.ts +5 -0
- package/extensions/subagents/src/id-sequence.ts +84 -0
- package/extensions/subagents/src/manager.ts +651 -536
- package/extensions/subagents/src/prompt.ts +185 -42
- package/extensions/subagents/src/result-artifact.ts +146 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +23 -6
- package/extensions/subagents/src/ui/takeover.ts +128 -337
- package/extensions/subagents/src/ui/transcript.ts +38 -501
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/tasks/ui.ts +79 -62
- package/extensions/ui-customization/footer.ts +7 -44
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +185 -0
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +147 -22
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/controller.ts +14 -2
- package/extensions/workflows/coordinator.ts +62 -0
- package/extensions/workflows/dashboard.ts +458 -339
- package/extensions/workflows/handoff.ts +121 -25
- package/extensions/workflows/index.ts +1042 -492
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +131 -19
- package/extensions/workflows/navigation.ts +61 -18
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +166 -10
- package/extensions/workflows/replay-safety.ts +58 -27
- package/extensions/workflows/result-delivery.ts +253 -0
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +36 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +28 -8
- package/skills/subagents/REFERENCE.md +189 -0
- package/skills/subagents/SKILL.md +2 -2
- package/skills/workflows/REFERENCE.md +10 -5
- package/skills/workflows/SKILL.md +53 -10
- package/web/adapter/pi-adapter.ts +661 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +789 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +462 -0
- package/web/runtime/pi-runtime.ts +991 -0
- package/web/runtime/types.ts +71 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1398 -0
- package/web/ui/index.html +139 -0
- package/web/ui/styles.css +598 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- package/extensions/subagents/src/backends/stub.ts +0 -296
- package/extensions/subagents/src/format.ts +0 -48
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentRecord,
|
|
3
|
+
AgentUsage,
|
|
4
|
+
WorkflowDetails,
|
|
5
|
+
WorkflowMemoryProjection,
|
|
6
|
+
} from "./model.ts";
|
|
7
|
+
import { toSerializable } from "./serialization.ts";
|
|
8
|
+
|
|
9
|
+
/** Defaults apply only to settled session-memory projections. Disk is canonical. */
|
|
10
|
+
export const DEFAULT_WORKFLOW_SETTLED_MAX_RUNS = 32;
|
|
11
|
+
export const DEFAULT_WORKFLOW_SETTLED_MAX_BYTES = 2 * 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
const MAX_PHASES = 32;
|
|
14
|
+
const MAX_LOGS = 8;
|
|
15
|
+
const MAX_NAME_BYTES = 512;
|
|
16
|
+
const MAX_DESCRIPTION_BYTES = 1_024;
|
|
17
|
+
const MAX_LABEL_BYTES = 256;
|
|
18
|
+
const MAX_PREVIEW_BYTES = 512;
|
|
19
|
+
const MAX_ERROR_BYTES = 1_024;
|
|
20
|
+
|
|
21
|
+
export interface WorkflowSettledRunRetentionOptions {
|
|
22
|
+
/** Maximum number of settled projections retained in this session. */
|
|
23
|
+
readonly maxRuns?: number;
|
|
24
|
+
/** Maximum serialized UTF-8 bytes retained by all projections. */
|
|
25
|
+
readonly maxBytes?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface WorkflowRetentionStats {
|
|
29
|
+
/** These counters describe only the current process/session epoch. */
|
|
30
|
+
readonly scope: "current-session";
|
|
31
|
+
readonly retainedRuns: number;
|
|
32
|
+
readonly retainedBytes: number;
|
|
33
|
+
/** Cumulative runs removed by count/byte pressure in this session. */
|
|
34
|
+
readonly evictedRuns: number;
|
|
35
|
+
/** Compatibility alias for evictedRuns. */
|
|
36
|
+
readonly settledRunsEvicted: number;
|
|
37
|
+
/** Serialized bytes belonging to projections removed by pressure. */
|
|
38
|
+
readonly evictedBytes: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function utf8Head(value: string, maxBytes: number) {
|
|
42
|
+
if (maxBytes <= 0) return "";
|
|
43
|
+
const bytes = Buffer.from(value, "utf8");
|
|
44
|
+
if (bytes.length <= maxBytes) return value;
|
|
45
|
+
let end = maxBytes;
|
|
46
|
+
while (end > 0 && (bytes[end] & 0xc0) === 0x80) end--;
|
|
47
|
+
return bytes.subarray(0, end).toString("utf8");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function bounded(value: string | undefined, maxBytes: number) {
|
|
51
|
+
return value === undefined ? undefined : utf8Head(value, maxBytes);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function jsonBytes(value: unknown) {
|
|
55
|
+
const serialized = JSON.stringify(toSerializable(value));
|
|
56
|
+
if (serialized === undefined)
|
|
57
|
+
throw new Error("Workflow projection is not serializable");
|
|
58
|
+
return Buffer.byteLength(serialized, "utf8");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function validateLimit(value: number, name: string) {
|
|
62
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
63
|
+
throw new Error(`${name} must be a non-negative safe integer`);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function finite(value: number | undefined) {
|
|
69
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
70
|
+
? value
|
|
71
|
+
: undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function compactUsage(usage: AgentUsage) {
|
|
75
|
+
return {
|
|
76
|
+
input: finite(usage.input) ?? 0,
|
|
77
|
+
output: finite(usage.output) ?? 0,
|
|
78
|
+
cacheRead: finite(usage.cacheRead) ?? 0,
|
|
79
|
+
cacheWrite: finite(usage.cacheWrite) ?? 0,
|
|
80
|
+
cost: finite(usage.cost) ?? 0,
|
|
81
|
+
...(finite(usage.contextTokens) !== undefined
|
|
82
|
+
? { contextTokens: usage.contextTokens }
|
|
83
|
+
: {}),
|
|
84
|
+
turns: finite(usage.turns) ?? 0,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function compactInvocation(agent: AgentRecord) {
|
|
89
|
+
const invocation = agent.invocation;
|
|
90
|
+
if (!invocation) return undefined;
|
|
91
|
+
return {
|
|
92
|
+
identity: {
|
|
93
|
+
runId: invocation.identity.runId,
|
|
94
|
+
callIndex: invocation.identity.callIndex,
|
|
95
|
+
},
|
|
96
|
+
intentState: invocation.intentState,
|
|
97
|
+
admissionState: invocation.admissionState,
|
|
98
|
+
executionState: invocation.executionState,
|
|
99
|
+
...(invocation.outcome ? { outcome: invocation.outcome } : {}),
|
|
100
|
+
requestedAt: invocation.requestedAt,
|
|
101
|
+
...(invocation.claimedAt !== undefined
|
|
102
|
+
? { claimedAt: invocation.claimedAt }
|
|
103
|
+
: {}),
|
|
104
|
+
...(invocation.runningAt !== undefined
|
|
105
|
+
? { runningAt: invocation.runningAt }
|
|
106
|
+
: {}),
|
|
107
|
+
...(invocation.terminalAt !== undefined
|
|
108
|
+
? { terminalAt: invocation.terminalAt }
|
|
109
|
+
: {}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function compactAgent(
|
|
114
|
+
agent: AgentRecord,
|
|
115
|
+
display: boolean,
|
|
116
|
+
): Record<string, unknown> {
|
|
117
|
+
const result: Record<string, unknown> = {
|
|
118
|
+
index: agent.index,
|
|
119
|
+
...(agent.callId ? { callId: agent.callId } : {}),
|
|
120
|
+
...(agent.invocation ? { invocation: compactInvocation(agent) } : {}),
|
|
121
|
+
label: bounded(agent.label, MAX_LABEL_BYTES) ?? "agent",
|
|
122
|
+
...(agent.phase ? { phase: bounded(agent.phase, MAX_LABEL_BYTES) } : {}),
|
|
123
|
+
state: agent.state,
|
|
124
|
+
...(agent.model ? { model: bounded(agent.model, MAX_LABEL_BYTES) } : {}),
|
|
125
|
+
...(agent.contextWindow !== undefined
|
|
126
|
+
? { contextWindow: agent.contextWindow }
|
|
127
|
+
: {}),
|
|
128
|
+
startedAt: agent.startedAt,
|
|
129
|
+
...(agent.finishedAt !== undefined ? { finishedAt: agent.finishedAt } : {}),
|
|
130
|
+
usage: compactUsage(agent.usage),
|
|
131
|
+
transcript: [],
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// These are recovery references, not display text. Truncating them would
|
|
135
|
+
// turn an otherwise recoverable artifact into an unusable path.
|
|
136
|
+
if (agent.resultArtifact) result.resultArtifact = agent.resultArtifact;
|
|
137
|
+
if (agent.resultRef) result.resultRef = agent.resultRef;
|
|
138
|
+
|
|
139
|
+
if (!display) return result;
|
|
140
|
+
if (agent.preview) result.preview = bounded(agent.preview, MAX_PREVIEW_BYTES);
|
|
141
|
+
if (agent.error) result.error = bounded(agent.error, MAX_ERROR_BYTES);
|
|
142
|
+
if (agent.replayed) result.replayed = true;
|
|
143
|
+
if (agent.operatorKey) result.operatorKey = agent.operatorKey;
|
|
144
|
+
if (agent.inputCallIds?.length)
|
|
145
|
+
result.inputCallIds = agent.inputCallIds.slice(0, 32);
|
|
146
|
+
if (agent.worktreeBranch) result.worktreeBranch = agent.worktreeBranch;
|
|
147
|
+
if (agent.worktreePath) result.worktreePath = agent.worktreePath;
|
|
148
|
+
if (agent.worktreeHandoffArtifact)
|
|
149
|
+
result.worktreeHandoffArtifact = agent.worktreeHandoffArtifact;
|
|
150
|
+
if (agent.worktreeCleanup) {
|
|
151
|
+
result.worktreeCleanup = {
|
|
152
|
+
removed: agent.worktreeCleanup.removed,
|
|
153
|
+
branchDeleted: agent.worktreeCleanup.branchDeleted,
|
|
154
|
+
branch: agent.worktreeCleanup.branch,
|
|
155
|
+
detached: agent.worktreeCleanup.detached,
|
|
156
|
+
...(agent.worktreeCleanup.reason
|
|
157
|
+
? { reason: bounded(agent.worktreeCleanup.reason, MAX_ERROR_BYTES) }
|
|
158
|
+
: {}),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (agent.acceptance) {
|
|
162
|
+
result.acceptance = {
|
|
163
|
+
status: agent.acceptance.status,
|
|
164
|
+
errors: agent.acceptance.errors
|
|
165
|
+
.slice(0, 4)
|
|
166
|
+
.map((error) => bounded(error, MAX_ERROR_BYTES)),
|
|
167
|
+
criteria: agent.acceptance.criteria.slice(0, 16).map((criterion) => ({
|
|
168
|
+
id: bounded(criterion.id, MAX_LABEL_BYTES),
|
|
169
|
+
status: criterion.status,
|
|
170
|
+
evidence: criterion.evidence
|
|
171
|
+
.slice(0, 4)
|
|
172
|
+
.map((evidence) => bounded(evidence, MAX_PREVIEW_BYTES)),
|
|
173
|
+
...(criterion.note
|
|
174
|
+
? { note: bounded(criterion.note, MAX_ERROR_BYTES) }
|
|
175
|
+
: {}),
|
|
176
|
+
})),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function omissionMetadata(
|
|
183
|
+
details: WorkflowDetails,
|
|
184
|
+
agentLimit: number,
|
|
185
|
+
logLimit: number,
|
|
186
|
+
): WorkflowMemoryProjection["omitted"] {
|
|
187
|
+
return {
|
|
188
|
+
agents: Math.max(0, details.agents.length - agentLimit),
|
|
189
|
+
logs:
|
|
190
|
+
Math.max(0, (details.logs?.length ?? 0) - logLimit) +
|
|
191
|
+
(details.logsDropped ?? 0),
|
|
192
|
+
transcriptEntries: details.agents.reduce(
|
|
193
|
+
(total, agent) => total + agent.transcript.length,
|
|
194
|
+
0,
|
|
195
|
+
),
|
|
196
|
+
result: details.result !== undefined,
|
|
197
|
+
graph: details.graph !== undefined,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function makeProjection(
|
|
202
|
+
details: WorkflowDetails,
|
|
203
|
+
cap: number,
|
|
204
|
+
agents: readonly Record<string, unknown>[],
|
|
205
|
+
logs: readonly unknown[],
|
|
206
|
+
agentLimit: number,
|
|
207
|
+
logLimit: number,
|
|
208
|
+
) {
|
|
209
|
+
const phases = details.phases.slice(0, MAX_PHASES).map((phase) => ({
|
|
210
|
+
title: bounded(phase.title, MAX_LABEL_BYTES) ?? "phase",
|
|
211
|
+
...(phase.detail
|
|
212
|
+
? { detail: bounded(phase.detail, MAX_DESCRIPTION_BYTES) }
|
|
213
|
+
: {}),
|
|
214
|
+
}));
|
|
215
|
+
const omitted = omissionMetadata(details, agentLimit, logLimit);
|
|
216
|
+
const candidate: Record<string, unknown> = {
|
|
217
|
+
runId: details.runId,
|
|
218
|
+
...(details.sessionId ? { sessionId: details.sessionId } : {}),
|
|
219
|
+
...(details.name ? { name: bounded(details.name, MAX_NAME_BYTES) } : {}),
|
|
220
|
+
...(details.description
|
|
221
|
+
? { description: bounded(details.description, MAX_DESCRIPTION_BYTES) }
|
|
222
|
+
: {}),
|
|
223
|
+
background: details.background,
|
|
224
|
+
status: details.status,
|
|
225
|
+
startedAt: details.startedAt,
|
|
226
|
+
...(details.finishedAt !== undefined
|
|
227
|
+
? { finishedAt: details.finishedAt }
|
|
228
|
+
: {}),
|
|
229
|
+
phases,
|
|
230
|
+
...(details.currentPhase
|
|
231
|
+
? { currentPhase: bounded(details.currentPhase, MAX_LABEL_BYTES) }
|
|
232
|
+
: {}),
|
|
233
|
+
agents,
|
|
234
|
+
...(logs.length > 0 ? { logs } : {}),
|
|
235
|
+
...(details.logsDropped ? { logsDropped: details.logsDropped } : {}),
|
|
236
|
+
...(details.delivery
|
|
237
|
+
? {
|
|
238
|
+
delivery: {
|
|
239
|
+
id: details.delivery.id,
|
|
240
|
+
state: details.delivery.state,
|
|
241
|
+
attempts: details.delivery.attempts,
|
|
242
|
+
updatedAt: details.delivery.updatedAt,
|
|
243
|
+
...(details.delivery.deliveredAt !== undefined
|
|
244
|
+
? { deliveredAt: details.delivery.deliveredAt }
|
|
245
|
+
: {}),
|
|
246
|
+
...(details.delivery.lastError
|
|
247
|
+
? {
|
|
248
|
+
lastError: bounded(
|
|
249
|
+
details.delivery.lastError,
|
|
250
|
+
MAX_ERROR_BYTES,
|
|
251
|
+
),
|
|
252
|
+
}
|
|
253
|
+
: {}),
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
: {}),
|
|
257
|
+
...(details.result !== undefined || details.resultArtifact
|
|
258
|
+
? {
|
|
259
|
+
result: details.resultArtifact
|
|
260
|
+
? "[stored in result.json]"
|
|
261
|
+
: "[result omitted from memory]",
|
|
262
|
+
...(details.resultArtifact
|
|
263
|
+
? { resultArtifact: details.resultArtifact }
|
|
264
|
+
: {}),
|
|
265
|
+
}
|
|
266
|
+
: {}),
|
|
267
|
+
...(details.transcriptArtifact
|
|
268
|
+
? { transcriptArtifact: details.transcriptArtifact }
|
|
269
|
+
: {}),
|
|
270
|
+
...(details.resumedFrom ? { resumedFrom: details.resumedFrom } : {}),
|
|
271
|
+
...(details.resumeNote
|
|
272
|
+
? { resumeNote: bounded(details.resumeNote, MAX_ERROR_BYTES) }
|
|
273
|
+
: {}),
|
|
274
|
+
...(details.error
|
|
275
|
+
? { error: bounded(details.error, MAX_ERROR_BYTES) }
|
|
276
|
+
: {}),
|
|
277
|
+
...(details.graph ? { graphOmitted: true } : {}),
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const sourceBytes = jsonBytes(details);
|
|
281
|
+
const projectedBytes = jsonBytes(candidate);
|
|
282
|
+
const omittedBytes = Math.max(0, sourceBytes - projectedBytes);
|
|
283
|
+
const metadata = (bytes: number): WorkflowMemoryProjection => ({
|
|
284
|
+
kind: "settled",
|
|
285
|
+
maxBytes: cap,
|
|
286
|
+
bytes,
|
|
287
|
+
truncated:
|
|
288
|
+
omittedBytes > 0 ||
|
|
289
|
+
omitted.agents > 0 ||
|
|
290
|
+
omitted.logs > 0 ||
|
|
291
|
+
omitted.transcriptEntries > 0 ||
|
|
292
|
+
omitted.result ||
|
|
293
|
+
omitted.graph,
|
|
294
|
+
omitted,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
return { candidate, metadata };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Build a detached settled-run projection. It intentionally never copies
|
|
302
|
+
* transcripts or arbitrary result values; those remain in the run artifacts.
|
|
303
|
+
*/
|
|
304
|
+
export function projectWorkflowDetails(
|
|
305
|
+
details: WorkflowDetails,
|
|
306
|
+
maxBytes = DEFAULT_WORKFLOW_SETTLED_MAX_BYTES,
|
|
307
|
+
): WorkflowDetails | undefined {
|
|
308
|
+
const cap = validateLimit(maxBytes, "maxBytes");
|
|
309
|
+
if (cap === 0) return undefined;
|
|
310
|
+
|
|
311
|
+
let display = true;
|
|
312
|
+
let agentLimit = details.agents.length;
|
|
313
|
+
let logLimit = Math.min(MAX_LOGS, details.logs?.length ?? 0);
|
|
314
|
+
let agents = details.agents.map((agent) => compactAgent(agent, display));
|
|
315
|
+
let logs = (details.logs ?? []).slice(-logLimit).map((log) => ({
|
|
316
|
+
at: log.at,
|
|
317
|
+
text: bounded(log.text, MAX_PREVIEW_BYTES) ?? "",
|
|
318
|
+
}));
|
|
319
|
+
|
|
320
|
+
const removeOptionalFields = () => {
|
|
321
|
+
display = false;
|
|
322
|
+
agents = details.agents.map((agent) => compactAgent(agent, false));
|
|
323
|
+
logs = [];
|
|
324
|
+
logLimit = 0;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const dropAgent = () => {
|
|
328
|
+
if (agents.length === 0) return false;
|
|
329
|
+
// Keep the oldest and newest identities preferentially while pressure
|
|
330
|
+
// removes middle display rows. Lifecycle facts for retained rows survive.
|
|
331
|
+
const index = agents.length > 1 ? Math.floor(agents.length / 2) : 0;
|
|
332
|
+
agents = [...agents.slice(0, index), ...agents.slice(index + 1)];
|
|
333
|
+
agentLimit--;
|
|
334
|
+
return true;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const dropRootOptional = (candidate: Record<string, unknown>) => {
|
|
338
|
+
for (const key of [
|
|
339
|
+
"description",
|
|
340
|
+
"resumeNote",
|
|
341
|
+
"error",
|
|
342
|
+
"currentPhase",
|
|
343
|
+
"name",
|
|
344
|
+
"sessionId",
|
|
345
|
+
"logsDropped",
|
|
346
|
+
"logs",
|
|
347
|
+
"graphOmitted",
|
|
348
|
+
"result",
|
|
349
|
+
]) {
|
|
350
|
+
if (key in candidate) {
|
|
351
|
+
delete candidate[key];
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return false;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
let candidate: Record<string, unknown> | undefined;
|
|
359
|
+
let projection: WorkflowMemoryProjection | undefined;
|
|
360
|
+
for (let pass = 0; pass < details.agents.length + 32; pass++) {
|
|
361
|
+
const built = makeProjection(
|
|
362
|
+
details,
|
|
363
|
+
cap,
|
|
364
|
+
agents,
|
|
365
|
+
logs,
|
|
366
|
+
agentLimit,
|
|
367
|
+
logLimit,
|
|
368
|
+
);
|
|
369
|
+
candidate = built.candidate;
|
|
370
|
+
projection = built.metadata(0);
|
|
371
|
+
candidate.memoryProjection = projection;
|
|
372
|
+
let bytes = jsonBytes(candidate);
|
|
373
|
+
if (bytes <= cap) {
|
|
374
|
+
// The byte count is itself part of the projection. Iterate until the
|
|
375
|
+
// decimal width of that field stabilizes.
|
|
376
|
+
for (let i = 0; i < 4; i++) {
|
|
377
|
+
candidate.memoryProjection = built.metadata(bytes);
|
|
378
|
+
const next = jsonBytes(candidate);
|
|
379
|
+
if (next === bytes) break;
|
|
380
|
+
bytes = next;
|
|
381
|
+
}
|
|
382
|
+
candidate.memoryProjection = built.metadata(bytes);
|
|
383
|
+
bytes = jsonBytes(candidate);
|
|
384
|
+
if (bytes <= cap) return candidate as unknown as WorkflowDetails;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (display) {
|
|
388
|
+
removeOptionalFields();
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (dropAgent()) continue;
|
|
392
|
+
if (dropRootOptional(candidate)) continue;
|
|
393
|
+
|
|
394
|
+
// The exact run id and terminal identity are non-negotiable. A caller
|
|
395
|
+
// using an impossibly small test budget gets an explicit non-retained run
|
|
396
|
+
// rather than a projection that cannot be addressed or measured safely.
|
|
397
|
+
const minimal: Record<string, unknown> = {
|
|
398
|
+
runId: details.runId,
|
|
399
|
+
background: details.background,
|
|
400
|
+
status: details.status,
|
|
401
|
+
startedAt: details.startedAt,
|
|
402
|
+
...(details.finishedAt !== undefined
|
|
403
|
+
? { finishedAt: details.finishedAt }
|
|
404
|
+
: {}),
|
|
405
|
+
phases: [],
|
|
406
|
+
agents: [],
|
|
407
|
+
...(details.resultArtifact
|
|
408
|
+
? { resultArtifact: details.resultArtifact }
|
|
409
|
+
: {}),
|
|
410
|
+
...(details.transcriptArtifact
|
|
411
|
+
? { transcriptArtifact: details.transcriptArtifact }
|
|
412
|
+
: {}),
|
|
413
|
+
};
|
|
414
|
+
const minimalProjection = makeProjection(
|
|
415
|
+
details,
|
|
416
|
+
cap,
|
|
417
|
+
[],
|
|
418
|
+
[],
|
|
419
|
+
0,
|
|
420
|
+
0,
|
|
421
|
+
).metadata(0);
|
|
422
|
+
minimal.memoryProjection = minimalProjection;
|
|
423
|
+
const minimalBytes = jsonBytes(minimal);
|
|
424
|
+
if (minimalBytes <= cap) {
|
|
425
|
+
minimal.memoryProjection = { ...minimalProjection, bytes: minimalBytes };
|
|
426
|
+
if (jsonBytes(minimal) <= cap)
|
|
427
|
+
return minimal as unknown as WorkflowDetails;
|
|
428
|
+
}
|
|
429
|
+
return undefined;
|
|
430
|
+
}
|
|
431
|
+
return undefined;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function measureWorkflowDetailsBytes(details: WorkflowDetails) {
|
|
435
|
+
return jsonBytes(details);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
interface RetainedEntry {
|
|
439
|
+
readonly details: WorkflowDetails;
|
|
440
|
+
readonly bytes: number;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Count- and byte-bounded insertion-ordered store for settled projections. */
|
|
444
|
+
export class WorkflowSettledRunRetention {
|
|
445
|
+
readonly maxRuns: number;
|
|
446
|
+
readonly maxBytes: number;
|
|
447
|
+
private readonly entries = new Map<string, RetainedEntry>();
|
|
448
|
+
private totalBytes = 0;
|
|
449
|
+
private totalEvictedRuns = 0;
|
|
450
|
+
private totalEvictedBytes = 0;
|
|
451
|
+
|
|
452
|
+
constructor(options: WorkflowSettledRunRetentionOptions = {}) {
|
|
453
|
+
this.maxRuns = validateLimit(
|
|
454
|
+
options.maxRuns ?? DEFAULT_WORKFLOW_SETTLED_MAX_RUNS,
|
|
455
|
+
"maxRuns",
|
|
456
|
+
);
|
|
457
|
+
this.maxBytes = validateLimit(
|
|
458
|
+
options.maxBytes ?? DEFAULT_WORKFLOW_SETTLED_MAX_BYTES,
|
|
459
|
+
"maxBytes",
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
set(details: WorkflowDetails) {
|
|
464
|
+
const previous = this.entries.get(details.runId);
|
|
465
|
+
if (previous) {
|
|
466
|
+
this.entries.delete(details.runId);
|
|
467
|
+
this.totalBytes -= previous.bytes;
|
|
468
|
+
}
|
|
469
|
+
const projection = projectWorkflowDetails(details, this.maxBytes);
|
|
470
|
+
if (!projection) {
|
|
471
|
+
// Keep a previously valid projection addressable when an unusually small
|
|
472
|
+
// configured budget cannot represent a later update. The canonical file
|
|
473
|
+
// remains the recovery source either way.
|
|
474
|
+
if (previous) {
|
|
475
|
+
this.entries.set(details.runId, previous);
|
|
476
|
+
this.totalBytes += previous.bytes;
|
|
477
|
+
} else {
|
|
478
|
+
this.totalEvictedRuns++;
|
|
479
|
+
}
|
|
480
|
+
return undefined;
|
|
481
|
+
}
|
|
482
|
+
const bytes = measureWorkflowDetailsBytes(projection);
|
|
483
|
+
this.entries.set(details.runId, { details: projection, bytes });
|
|
484
|
+
this.totalBytes += bytes;
|
|
485
|
+
while (
|
|
486
|
+
this.entries.size > this.maxRuns ||
|
|
487
|
+
this.totalBytes > this.maxBytes
|
|
488
|
+
) {
|
|
489
|
+
const oldest = this.entries.keys().next().value;
|
|
490
|
+
if (oldest === undefined) break;
|
|
491
|
+
const entry = this.entries.get(oldest);
|
|
492
|
+
this.entries.delete(oldest);
|
|
493
|
+
if (!entry) continue;
|
|
494
|
+
this.totalBytes -= entry.bytes;
|
|
495
|
+
this.totalEvictedRuns++;
|
|
496
|
+
this.totalEvictedBytes += entry.bytes;
|
|
497
|
+
}
|
|
498
|
+
return this.entries.get(details.runId)?.details;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
get(runId: string) {
|
|
502
|
+
return this.entries.get(runId)?.details;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
has(runId: string) {
|
|
506
|
+
return this.entries.has(runId);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
delete(runId: string) {
|
|
510
|
+
const entry = this.entries.get(runId);
|
|
511
|
+
if (!entry) return false;
|
|
512
|
+
this.entries.delete(runId);
|
|
513
|
+
this.totalBytes -= entry.bytes;
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
clear() {
|
|
518
|
+
this.entries.clear();
|
|
519
|
+
this.totalBytes = 0;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Clear retained projections without changing current-session counters. */
|
|
523
|
+
reset() {
|
|
524
|
+
this.clear();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Start a new session accounting epoch. */
|
|
528
|
+
resetStats() {
|
|
529
|
+
this.totalEvictedRuns = 0;
|
|
530
|
+
this.totalEvictedBytes = 0;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Clear projections and start a new session accounting epoch. */
|
|
534
|
+
resetSession() {
|
|
535
|
+
this.clear();
|
|
536
|
+
this.resetStats();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
keys() {
|
|
540
|
+
return this.entries.keys();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
values() {
|
|
544
|
+
return [...this.entries.values()].map((entry) => entry.details).values();
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
entriesArray() {
|
|
548
|
+
return [...this.entries.entries()].map(
|
|
549
|
+
([id, entry]) => [id, entry.details] as const,
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
get size() {
|
|
554
|
+
return this.entries.size;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
get retainedBytes() {
|
|
558
|
+
return this.totalBytes;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
get evictedRuns() {
|
|
562
|
+
return this.totalEvictedRuns;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
get settledRunsEvicted() {
|
|
566
|
+
return this.totalEvictedRuns;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
get evictedBytes() {
|
|
570
|
+
return this.totalEvictedBytes;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
get stats(): WorkflowRetentionStats {
|
|
574
|
+
return {
|
|
575
|
+
scope: "current-session",
|
|
576
|
+
retainedRuns: this.entries.size,
|
|
577
|
+
retainedBytes: this.totalBytes,
|
|
578
|
+
evictedRuns: this.totalEvictedRuns,
|
|
579
|
+
settledRunsEvicted: this.totalEvictedRuns,
|
|
580
|
+
evictedBytes: this.totalEvictedBytes,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
getStats() {
|
|
585
|
+
return this.stats;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export function createWorkflowSettledRunRetention(
|
|
590
|
+
options: WorkflowSettledRunRetentionOptions = {},
|
|
591
|
+
) {
|
|
592
|
+
return new WorkflowSettledRunRetention(options);
|
|
593
|
+
}
|