@tt-a1i/openpi 0.4.0 → 0.6.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 +116 -46
- package/SETUP.md +29 -7
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +155 -0
- package/extensions/ai-providers/LICENSE.upstream +23 -0
- package/extensions/ai-providers/README.md +59 -0
- package/extensions/ai-providers/antigravity/credentials.ts +52 -0
- package/extensions/ai-providers/antigravity/discovery.ts +130 -0
- package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
- package/extensions/ai-providers/antigravity/models.ts +84 -0
- package/extensions/ai-providers/antigravity/oauth.ts +700 -0
- package/extensions/ai-providers/antigravity/provider.ts +1116 -0
- package/extensions/ai-providers/antigravity/routing.ts +340 -0
- package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
- package/extensions/ai-providers/cursor/constants.ts +5 -0
- package/extensions/ai-providers/cursor/credentials.ts +14 -0
- package/extensions/ai-providers/cursor/discovery.ts +291 -0
- package/extensions/ai-providers/cursor/input-images.ts +106 -0
- package/extensions/ai-providers/cursor/models.ts +45 -0
- package/extensions/ai-providers/cursor/oauth.ts +263 -0
- package/extensions/ai-providers/cursor/proto.ts +1064 -0
- package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
- package/extensions/ai-providers/cursor/provider.ts +1175 -0
- package/extensions/ai-providers/cursor/proxy.ts +213 -0
- package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
- package/extensions/ai-providers/index.ts +86 -0
- package/extensions/ai-providers/oauth-adapter.ts +81 -0
- package/extensions/ai-providers/usage.ts +10 -0
- package/extensions/background-terminals/index.ts +38 -3
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +484 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +13 -5
- package/extensions/background-terminals/src/result-delivery.ts +47 -24
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/index.ts +68 -27
- package/extensions/cron/schedule.ts +12 -2
- package/extensions/file-mutation-display/render.ts +17 -257
- package/extensions/file-search/src/binaries.ts +57 -41
- package/extensions/git-read/index.ts +1 -3
- package/extensions/model-info/cache-diagnostics.ts +220 -0
- package/extensions/model-info/index.ts +65 -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 +82 -6
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +153 -86
- 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 +138 -130
- package/extensions/shared/activity-status.ts +30 -0
- 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/capability-intent.ts +1 -1
- package/extensions/shared/child-session.ts +457 -21
- package/extensions/shared/completion-inbox.ts +193 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/setup-config.ts +83 -34
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/structured-output.ts +154 -0
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +72 -15
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +29 -2
- 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 +313 -62
- package/extensions/subagents/navigation.ts +34 -5
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +450 -70
- package/extensions/subagents/src/domain.ts +21 -1
- package/extensions/subagents/src/manager.ts +39 -2
- package/extensions/subagents/src/prompt.ts +49 -7
- package/extensions/subagents/src/result-artifact.ts +36 -0
- package/extensions/subagents/src/result-delivery.ts +39 -14
- package/extensions/subagents/src/runtime.ts +15 -1
- package/extensions/subagents/src/ui/takeover.ts +73 -257
- package/extensions/subagents/src/ui/transcript.ts +38 -535
- 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/ui-customization/footer.ts +16 -45
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +42 -6
- package/extensions/web/index.ts +257 -0
- package/extensions/workflows/acceptance.ts +43 -19
- package/extensions/workflows/artifacts.ts +137 -47
- package/extensions/workflows/completion-projection.ts +459 -0
- package/extensions/workflows/coordinator.ts +8 -10
- package/extensions/workflows/dashboard.ts +175 -228
- package/extensions/workflows/handoff.ts +70 -16
- package/extensions/workflows/index.ts +501 -198
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +79 -5
- package/extensions/workflows/navigation.ts +32 -8
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +70 -16
- package/extensions/workflows/replay-safety.ts +42 -21
- package/extensions/workflows/result-delivery.ts +214 -76
- package/extensions/workflows/retention.ts +599 -0
- package/extensions/workflows/runner.ts +389 -345
- package/extensions/workflows/sandbox-child.cjs +25 -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 +34 -14
- package/skills/subagents/REFERENCE.md +190 -0
- package/skills/subagents/SKILL.md +2 -1
- package/skills/workflows/REFERENCE.md +6 -4
- package/skills/workflows/SKILL.md +1 -1
- package/web/adapter/pi-adapter.ts +664 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/pi-coding-agent-entry.ts +162 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +1069 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +467 -0
- package/web/runtime/pi-runtime.ts +1206 -0
- package/web/runtime/types.ts +102 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1700 -0
- package/web/ui/index.html +142 -0
- package/web/ui/styles.css +680 -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 -303
|
@@ -38,6 +38,148 @@ export interface WorkflowJournal {
|
|
|
38
38
|
readonly entries: readonly JournalEntry[];
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
interface EncodedJournalEntry {
|
|
42
|
+
readonly entry: JournalEntry;
|
|
43
|
+
/** The entry formatted at the indentation used inside the journal array. */
|
|
44
|
+
readonly json: string;
|
|
45
|
+
readonly bytes: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const JOURNAL_PREFIX = `{
|
|
49
|
+
"version": ${JOURNAL_VERSION},
|
|
50
|
+
"entries": [
|
|
51
|
+
`;
|
|
52
|
+
const JOURNAL_SUFFIX = "\n ]\n}";
|
|
53
|
+
const JOURNAL_SEPARATOR = ",\n";
|
|
54
|
+
const JOURNAL_EMPTY = JSON.stringify(
|
|
55
|
+
{ version: JOURNAL_VERSION, entries: [] },
|
|
56
|
+
null,
|
|
57
|
+
2,
|
|
58
|
+
);
|
|
59
|
+
const JOURNAL_PREFIX_BYTES = Buffer.byteLength(JOURNAL_PREFIX, "utf8");
|
|
60
|
+
const JOURNAL_SUFFIX_BYTES = Buffer.byteLength(JOURNAL_SUFFIX, "utf8");
|
|
61
|
+
const JOURNAL_SEPARATOR_BYTES = Buffer.byteLength(JOURNAL_SEPARATOR, "utf8");
|
|
62
|
+
const JOURNAL_EMPTY_BYTES = Buffer.byteLength(JOURNAL_EMPTY, "utf8");
|
|
63
|
+
|
|
64
|
+
function encodeJournalEntry(entry: JournalEntry): EncodedJournalEntry {
|
|
65
|
+
// JSON.stringify on an array serializes an undefined element as null. The
|
|
66
|
+
// public type only permits objects, but retaining that fallback keeps this
|
|
67
|
+
// helper's behavior total if a malformed caller reaches it at runtime.
|
|
68
|
+
const json = JSON.stringify(entry, null, 2) ?? "null";
|
|
69
|
+
const indented = ` ${json.replaceAll("\n", "\n ")}`;
|
|
70
|
+
return {
|
|
71
|
+
entry,
|
|
72
|
+
json: indented,
|
|
73
|
+
bytes: Buffer.byteLength(indented, "utf8"),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function journalBytes(entryCount: number, entryBytes: number) {
|
|
78
|
+
if (entryCount === 0) return JOURNAL_EMPTY_BYTES;
|
|
79
|
+
return (
|
|
80
|
+
JOURNAL_PREFIX_BYTES +
|
|
81
|
+
entryBytes +
|
|
82
|
+
JOURNAL_SUFFIX_BYTES +
|
|
83
|
+
(entryCount - 1) * JOURNAL_SEPARATOR_BYTES
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Incrementally bounded journal storage for a running Workflow.
|
|
89
|
+
*
|
|
90
|
+
* Each entry is encoded once when appended. The active queue uses a head
|
|
91
|
+
* cursor, so dropping old entries only subtracts their already-known byte
|
|
92
|
+
* contribution. `toJson()` assembles the complete canonical artifact without
|
|
93
|
+
* re-stringifying each entry or repeatedly measuring the whole suffix.
|
|
94
|
+
*/
|
|
95
|
+
export interface WorkflowJournalAccumulator {
|
|
96
|
+
readonly bytes: number;
|
|
97
|
+
readonly dropped: number;
|
|
98
|
+
readonly length: number;
|
|
99
|
+
append(entry: JournalEntry): void;
|
|
100
|
+
toJournal(): WorkflowJournal;
|
|
101
|
+
toJson(): string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function createJournalAccumulator(
|
|
105
|
+
maxBytes = JOURNAL_MAX_BYTES,
|
|
106
|
+
): WorkflowJournalAccumulator {
|
|
107
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < JOURNAL_EMPTY_BYTES) {
|
|
108
|
+
throw new RangeError(
|
|
109
|
+
`Journal byte budget must be at least ${JOURNAL_EMPTY_BYTES} bytes`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let queue: Array<EncodedJournalEntry | undefined> = [];
|
|
114
|
+
let head = 0;
|
|
115
|
+
let entryBytes = 0;
|
|
116
|
+
let dropped = 0;
|
|
117
|
+
|
|
118
|
+
const activeLength = () => queue.length - head;
|
|
119
|
+
const activeBytes = () => journalBytes(activeLength(), entryBytes);
|
|
120
|
+
|
|
121
|
+
const compact = () => {
|
|
122
|
+
// Release the encoded payload immediately when it is evicted. Compact the
|
|
123
|
+
// sparse prefix only occasionally so eviction remains amortized O(1).
|
|
124
|
+
if (head === queue.length) {
|
|
125
|
+
queue = [];
|
|
126
|
+
head = 0;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (head >= 64 && head * 2 >= queue.length) {
|
|
130
|
+
queue = queue.slice(head);
|
|
131
|
+
head = 0;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
get bytes() {
|
|
137
|
+
return activeBytes();
|
|
138
|
+
},
|
|
139
|
+
get dropped() {
|
|
140
|
+
return dropped;
|
|
141
|
+
},
|
|
142
|
+
get length() {
|
|
143
|
+
return activeLength();
|
|
144
|
+
},
|
|
145
|
+
append(entry) {
|
|
146
|
+
const encoded = encodeJournalEntry(entry);
|
|
147
|
+
queue.push(encoded);
|
|
148
|
+
entryBytes += encoded.bytes;
|
|
149
|
+
|
|
150
|
+
while (activeLength() > 0 && activeBytes() > maxBytes) {
|
|
151
|
+
const oldest = queue[head];
|
|
152
|
+
if (!oldest) break;
|
|
153
|
+
queue[head] = undefined;
|
|
154
|
+
head++;
|
|
155
|
+
entryBytes -= oldest.bytes;
|
|
156
|
+
dropped++;
|
|
157
|
+
}
|
|
158
|
+
compact();
|
|
159
|
+
},
|
|
160
|
+
toJournal() {
|
|
161
|
+
const entries: JournalEntry[] = [];
|
|
162
|
+
for (let index = head; index < queue.length; index++) {
|
|
163
|
+
const encoded = queue[index];
|
|
164
|
+
if (encoded) entries.push(encoded.entry);
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
version: JOURNAL_VERSION,
|
|
168
|
+
entries,
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
toJson() {
|
|
172
|
+
if (activeLength() === 0) return JOURNAL_EMPTY;
|
|
173
|
+
const entries: string[] = [];
|
|
174
|
+
for (let index = head; index < queue.length; index++) {
|
|
175
|
+
const encoded = queue[index];
|
|
176
|
+
if (encoded) entries.push(encoded.json);
|
|
177
|
+
}
|
|
178
|
+
return JOURNAL_PREFIX + entries.join(JOURNAL_SEPARATOR) + JOURNAL_SUFFIX;
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
41
183
|
/**
|
|
42
184
|
* Only semantic options change what an agent produces, so only these are
|
|
43
185
|
* hashed. Callers pass their whole options object; `label` and `phase` are
|
|
@@ -146,19 +288,12 @@ export type ReplayCache = ReturnType<typeof createReplayCache>;
|
|
|
146
288
|
* journal that serializes to a useless preview string.
|
|
147
289
|
*/
|
|
148
290
|
export function boundedJournal(entries: readonly JournalEntry[]) {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
let kept = [...entries];
|
|
156
|
-
let dropped = 0;
|
|
157
|
-
while (kept.length > 0 && size(kept) > JOURNAL_MAX_BYTES) {
|
|
158
|
-
kept = kept.slice(1);
|
|
159
|
-
dropped++;
|
|
160
|
-
}
|
|
161
|
-
return { journal: { version: JOURNAL_VERSION, entries: kept }, dropped };
|
|
291
|
+
const accumulator = createJournalAccumulator();
|
|
292
|
+
for (const entry of entries) accumulator.append(entry);
|
|
293
|
+
return {
|
|
294
|
+
journal: accumulator.toJournal(),
|
|
295
|
+
dropped: accumulator.dropped,
|
|
296
|
+
};
|
|
162
297
|
}
|
|
163
298
|
|
|
164
299
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import {
|
|
8
|
-
truncateHead,
|
|
9
8
|
type ExtensionContext,
|
|
9
|
+
truncateHead,
|
|
10
10
|
} from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { formatContextUtilization } from "../shared/context-utilization.ts";
|
|
12
12
|
import { spinnerFrame } from "../shared/spinner.ts";
|
|
@@ -67,6 +67,10 @@ export type WorkflowDeliveryState =
|
|
|
67
67
|
export interface WorkflowDelivery {
|
|
68
68
|
/** Stable per-run idempotency identity, never a transport-batch id. */
|
|
69
69
|
id: string;
|
|
70
|
+
/** Destination transcript identity; legacy records fall back to run.sessionId. */
|
|
71
|
+
ownerSessionId?: string;
|
|
72
|
+
/** Process-local generation of the Pi SessionManager owner. */
|
|
73
|
+
ownerEpoch?: number;
|
|
70
74
|
state: WorkflowDeliveryState;
|
|
71
75
|
attempts: number;
|
|
72
76
|
updatedAt: number;
|
|
@@ -123,7 +127,7 @@ export interface AgentRecord {
|
|
|
123
127
|
usage: AgentUsage;
|
|
124
128
|
/** Replayed from a prior run's journal instead of actually executed. */
|
|
125
129
|
replayed?: boolean;
|
|
126
|
-
/**
|
|
130
|
+
/** Deprecated child self-attestation; never runtime-observed evidence. */
|
|
127
131
|
acceptance?: AcceptanceLedger;
|
|
128
132
|
/** Branch of the isolated worktree this agent ran in, when it holds commits. */
|
|
129
133
|
worktreeBranch?: string;
|
|
@@ -141,6 +145,23 @@ export interface AgentRecord {
|
|
|
141
145
|
export interface WorkflowLogEntry {
|
|
142
146
|
at: number;
|
|
143
147
|
text: string;
|
|
148
|
+
/** Runtime-authored evidence, distinct from free-form script narration. */
|
|
149
|
+
kind?: "pipeline-drop";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Metadata attached only to the bounded in-memory settled-run projection. */
|
|
153
|
+
export interface WorkflowMemoryProjection {
|
|
154
|
+
readonly kind: "settled";
|
|
155
|
+
readonly maxBytes: number;
|
|
156
|
+
readonly bytes: number;
|
|
157
|
+
readonly truncated: boolean;
|
|
158
|
+
readonly omitted: {
|
|
159
|
+
readonly agents: number;
|
|
160
|
+
readonly logs: number;
|
|
161
|
+
readonly transcriptEntries: number;
|
|
162
|
+
readonly result: boolean;
|
|
163
|
+
readonly graph: boolean;
|
|
164
|
+
};
|
|
144
165
|
}
|
|
145
166
|
|
|
146
167
|
export interface WorkflowDetails {
|
|
@@ -172,6 +193,54 @@ export interface WorkflowDetails {
|
|
|
172
193
|
/** Read-only lineage projection; never execution or admission authority. */
|
|
173
194
|
graph?: WorkflowGraphProjection;
|
|
174
195
|
error?: string;
|
|
196
|
+
/** Present only on the session-memory projection, never canonical history. */
|
|
197
|
+
memoryProjection?: WorkflowMemoryProjection;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Bounded tool-result projection; authoritative details remain in run artifacts. */
|
|
201
|
+
export function compactWorkflowToolDetails(
|
|
202
|
+
details: WorkflowDetails,
|
|
203
|
+
): WorkflowDetails {
|
|
204
|
+
return {
|
|
205
|
+
...details,
|
|
206
|
+
...(details.result !== undefined
|
|
207
|
+
? {
|
|
208
|
+
result: JSON.parse(
|
|
209
|
+
safeStringify(details.result, { maxBytes: 64 * 1024 }),
|
|
210
|
+
),
|
|
211
|
+
}
|
|
212
|
+
: {}),
|
|
213
|
+
agents: details.agents.map((agent) => ({ ...agent, transcript: [] })),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Bound only the current-session terminal projection. Persisted workflow
|
|
219
|
+
* records and side artifacts remain the canonical history.
|
|
220
|
+
*/
|
|
221
|
+
export const MAX_SETTLED_RUNS = 32;
|
|
222
|
+
|
|
223
|
+
/** Evict the oldest terminal projections without touching active-run state. */
|
|
224
|
+
export function evictOldestSettledRuns(
|
|
225
|
+
settledRuns: Map<string, WorkflowDetails>,
|
|
226
|
+
maxRuns = MAX_SETTLED_RUNS,
|
|
227
|
+
): string[] {
|
|
228
|
+
const limit = Number.isFinite(maxRuns)
|
|
229
|
+
? Math.max(0, Math.floor(maxRuns))
|
|
230
|
+
: MAX_SETTLED_RUNS;
|
|
231
|
+
const evicted: string[] = [];
|
|
232
|
+
while (settledRuns.size > limit) {
|
|
233
|
+
let oldest: { runId: string; at: number } | undefined;
|
|
234
|
+
for (const [runId, details] of settledRuns) {
|
|
235
|
+
if (details.status === "running") continue;
|
|
236
|
+
const at = details.finishedAt ?? details.startedAt;
|
|
237
|
+
if (!oldest || at < oldest.at) oldest = { runId, at };
|
|
238
|
+
}
|
|
239
|
+
if (!oldest) break;
|
|
240
|
+
settledRuns.delete(oldest.runId);
|
|
241
|
+
evicted.push(oldest.runId);
|
|
242
|
+
}
|
|
243
|
+
return evicted;
|
|
175
244
|
}
|
|
176
245
|
|
|
177
246
|
export function workflowGraphRecords(
|
|
@@ -306,11 +375,12 @@ export function appendLog(
|
|
|
306
375
|
details: WorkflowDetails,
|
|
307
376
|
text: string,
|
|
308
377
|
at: number,
|
|
378
|
+
kind?: WorkflowLogEntry["kind"],
|
|
309
379
|
): void {
|
|
310
380
|
const clean = sanitizeLine(text, MAX_LOG_TEXT);
|
|
311
381
|
if (!clean) return;
|
|
312
382
|
const logs = (details.logs ??= []);
|
|
313
|
-
logs.push({ at, text: clean });
|
|
383
|
+
logs.push({ at, text: clean, ...(kind ? { kind } : {}) });
|
|
314
384
|
const excess = logs.length - MAX_LOG_ENTRIES;
|
|
315
385
|
if (excess > 0) {
|
|
316
386
|
logs.splice(0, excess);
|
|
@@ -432,10 +502,14 @@ export function agentContext(agent: AgentRecord): string {
|
|
|
432
502
|
});
|
|
433
503
|
}
|
|
434
504
|
|
|
435
|
-
export function formatElapsed(
|
|
505
|
+
export function formatElapsed(
|
|
506
|
+
startedAt: number,
|
|
507
|
+
finishedAt?: number,
|
|
508
|
+
now = Date.now(),
|
|
509
|
+
): string {
|
|
436
510
|
const totalSeconds = Math.max(
|
|
437
511
|
0,
|
|
438
|
-
Math.round(((finishedAt ??
|
|
512
|
+
Math.round(((finishedAt ?? now) - startedAt) / 1000),
|
|
439
513
|
);
|
|
440
514
|
const minutes = Math.floor(totalSeconds / 60);
|
|
441
515
|
const seconds = totalSeconds % 60;
|
|
@@ -31,6 +31,11 @@ export interface WorkflowStripEntry {
|
|
|
31
31
|
details: WorkflowDetails;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/** Changes only when the strip needs an immediate lifecycle repaint. */
|
|
35
|
+
export function workflowStripEntryKey(entry: WorkflowStripEntry | undefined) {
|
|
36
|
+
return entry ? `${entry.runId}:${entry.details.status}` : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
function cleanLine(value: string) {
|
|
35
40
|
return sanitizeTerminalText(value).replace(/\s+/g, " ").trim();
|
|
36
41
|
}
|
|
@@ -48,7 +53,7 @@ function statusGlyph(status: WorkflowStatus, theme: Theme, now: number) {
|
|
|
48
53
|
|
|
49
54
|
/** Live, one-line Claude-style workflow entry rendered below the editor. */
|
|
50
55
|
export class WorkflowStripWidget {
|
|
51
|
-
private
|
|
56
|
+
private timer: ReturnType<typeof setInterval> | undefined;
|
|
52
57
|
private readonly tui: TUI;
|
|
53
58
|
private readonly theme: Theme;
|
|
54
59
|
private readonly strip: BelowEditorStripState;
|
|
@@ -64,20 +69,39 @@ export class WorkflowStripWidget {
|
|
|
64
69
|
this.theme = theme;
|
|
65
70
|
this.strip = strip;
|
|
66
71
|
this.getEntry = getEntry;
|
|
67
|
-
this.
|
|
68
|
-
() => this.tui.requestRender(),
|
|
69
|
-
SPINNER_INTERVAL_MS,
|
|
70
|
-
);
|
|
71
|
-
this.timer.unref?.();
|
|
72
|
+
this.syncSpinner();
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
dispose() {
|
|
75
|
-
clearInterval(this.timer);
|
|
76
|
+
if (this.timer) clearInterval(this.timer);
|
|
77
|
+
this.timer = undefined;
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
invalidate() {
|
|
80
|
+
invalidate() {
|
|
81
|
+
this.syncSpinner();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private syncSpinner() {
|
|
85
|
+
if (this.getEntry()?.details.status === "running") {
|
|
86
|
+
if (this.timer) return;
|
|
87
|
+
const timer = setInterval(() => {
|
|
88
|
+
if (this.timer !== timer) return;
|
|
89
|
+
if (this.getEntry()?.details.status !== "running") {
|
|
90
|
+
clearInterval(timer);
|
|
91
|
+
this.timer = undefined;
|
|
92
|
+
}
|
|
93
|
+
this.tui.requestRender();
|
|
94
|
+
}, SPINNER_INTERVAL_MS);
|
|
95
|
+
this.timer = timer;
|
|
96
|
+
timer.unref?.();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (this.timer) clearInterval(this.timer);
|
|
100
|
+
this.timer = undefined;
|
|
101
|
+
}
|
|
79
102
|
|
|
80
103
|
render(width: number) {
|
|
104
|
+
this.syncSpinner();
|
|
81
105
|
const entry = this.getEntry();
|
|
82
106
|
if (!entry || width <= 0) return [];
|
|
83
107
|
const details = entry.details;
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import {
|
|
2
|
+
calculateContextTokens,
|
|
3
|
+
type AgentSession,
|
|
4
|
+
estimateTokens,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { type AgentUsage, emptyUsage, type TranscriptEntry } from "./model.ts";
|
|
7
|
+
import { safeStringify, truncateUtf8 } from "./serialization.ts";
|
|
8
|
+
|
|
9
|
+
const TRANSCRIPT_ENTRY_MAX_BYTES = 16 * 1024;
|
|
10
|
+
const TRANSCRIPT_TOTAL_MAX_BYTES = 256 * 1024;
|
|
11
|
+
const TRANSCRIPT_MAX_ENTRIES = 200;
|
|
12
|
+
|
|
13
|
+
type AgentMessage = AgentSession["messages"][number];
|
|
14
|
+
export type ProgressAssistantMessage = Extract<
|
|
15
|
+
AgentMessage,
|
|
16
|
+
{ role: "assistant" }
|
|
17
|
+
>;
|
|
18
|
+
|
|
19
|
+
export interface ProgressToolTiming {
|
|
20
|
+
startedAt?: number;
|
|
21
|
+
finishedAt?: number;
|
|
22
|
+
durationMs?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type ContextTokens = number | null | undefined;
|
|
26
|
+
|
|
27
|
+
interface ProjectedTranscriptEntry {
|
|
28
|
+
role: TranscriptEntry["role"];
|
|
29
|
+
text: string;
|
|
30
|
+
sourceTextTruncated: boolean;
|
|
31
|
+
name?: string;
|
|
32
|
+
sourceToolCallId?: string;
|
|
33
|
+
isError?: boolean;
|
|
34
|
+
timestamp?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AgentProgressProjectionSnapshot {
|
|
38
|
+
preview: string;
|
|
39
|
+
usage: AgentUsage;
|
|
40
|
+
latestAssistant?: ProgressAssistantMessage;
|
|
41
|
+
transcript: TranscriptEntry[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validAssistantContextTokens(message: ProgressAssistantMessage) {
|
|
45
|
+
if (
|
|
46
|
+
message.stopReason === "aborted" ||
|
|
47
|
+
message.stopReason === "error" ||
|
|
48
|
+
!message.usage
|
|
49
|
+
) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const tokens = calculateContextTokens(message.usage);
|
|
53
|
+
return tokens > 0 ? tokens : undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function boundedSourceText(text: string) {
|
|
57
|
+
const bounded = truncateUtf8(text, TRANSCRIPT_ENTRY_MAX_BYTES);
|
|
58
|
+
return { text: bounded, sourceTextTruncated: bounded !== text };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function safeJson(value: unknown) {
|
|
62
|
+
return safeStringify(value, {
|
|
63
|
+
maxBytes: TRANSCRIPT_ENTRY_MAX_BYTES,
|
|
64
|
+
maxDepth: 12,
|
|
65
|
+
maxNodes: 2_000,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function messageEntries(message: AgentMessage): ProjectedTranscriptEntry[] {
|
|
70
|
+
if (message.role === "user") {
|
|
71
|
+
const source =
|
|
72
|
+
typeof message.content === "string"
|
|
73
|
+
? message.content
|
|
74
|
+
: message.content
|
|
75
|
+
.map((part) =>
|
|
76
|
+
part.type === "text" ? part.text : `[image: ${part.mimeType}]`,
|
|
77
|
+
)
|
|
78
|
+
.join("\n");
|
|
79
|
+
if (!source.trim()) return [];
|
|
80
|
+
return [
|
|
81
|
+
{
|
|
82
|
+
role: "user",
|
|
83
|
+
...boundedSourceText(source),
|
|
84
|
+
timestamp: message.timestamp,
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (message.role === "assistant") {
|
|
90
|
+
const entries: ProjectedTranscriptEntry[] = [];
|
|
91
|
+
for (const part of message.content) {
|
|
92
|
+
if (part.type === "text" && part.text.trim()) {
|
|
93
|
+
entries.push({
|
|
94
|
+
role: "assistant",
|
|
95
|
+
...boundedSourceText(part.text),
|
|
96
|
+
timestamp: message.timestamp,
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (part.type === "thinking" && part.thinking.trim()) {
|
|
101
|
+
entries.push({
|
|
102
|
+
role: "thinking",
|
|
103
|
+
...boundedSourceText(part.thinking),
|
|
104
|
+
timestamp: message.timestamp,
|
|
105
|
+
});
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (part.type === "toolCall") {
|
|
109
|
+
entries.push({
|
|
110
|
+
role: "tool",
|
|
111
|
+
...boundedSourceText(safeJson(part.arguments)),
|
|
112
|
+
name: part.name,
|
|
113
|
+
sourceToolCallId: part.id,
|
|
114
|
+
timestamp: message.timestamp,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return entries;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (message.role !== "toolResult") return [];
|
|
122
|
+
const source = message.content
|
|
123
|
+
.map((part) =>
|
|
124
|
+
part.type === "text" ? part.text : `[image: ${part.mimeType}]`,
|
|
125
|
+
)
|
|
126
|
+
.join("\n");
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
role: "toolResult",
|
|
130
|
+
...boundedSourceText(source),
|
|
131
|
+
name: message.toolName,
|
|
132
|
+
sourceToolCallId: message.toolCallId,
|
|
133
|
+
isError: message.isError,
|
|
134
|
+
timestamp: message.timestamp,
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function assistantText(message: ProgressAssistantMessage) {
|
|
140
|
+
return message.content
|
|
141
|
+
.filter((part) => part.type === "text")
|
|
142
|
+
.map((part) => part.text)
|
|
143
|
+
.join("\n")
|
|
144
|
+
.trim();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function addAssistantUsage(
|
|
148
|
+
usage: AgentUsage,
|
|
149
|
+
message: ProgressAssistantMessage,
|
|
150
|
+
) {
|
|
151
|
+
usage.turns++;
|
|
152
|
+
const current = message.usage;
|
|
153
|
+
if (!current) return;
|
|
154
|
+
usage.input += current.input || 0;
|
|
155
|
+
usage.output += current.output || 0;
|
|
156
|
+
usage.cacheRead += current.cacheRead || 0;
|
|
157
|
+
usage.cacheWrite += current.cacheWrite || 0;
|
|
158
|
+
usage.cost += current.cost?.total || 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function toolMetadata(
|
|
162
|
+
toolCallId: string,
|
|
163
|
+
timings: ReadonlyMap<string, ProgressToolTiming>,
|
|
164
|
+
) {
|
|
165
|
+
const timing = timings.get(toolCallId);
|
|
166
|
+
return {
|
|
167
|
+
toolCallId: truncateUtf8(toolCallId, 1024),
|
|
168
|
+
...(timing?.startedAt === undefined ? {} : { startedAt: timing.startedAt }),
|
|
169
|
+
...(timing?.finishedAt === undefined
|
|
170
|
+
? {}
|
|
171
|
+
: { finishedAt: timing.finishedAt }),
|
|
172
|
+
...(timing?.durationMs === undefined
|
|
173
|
+
? {}
|
|
174
|
+
: { durationMs: timing.durationMs }),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Bounded, ephemeral projection of Pi's active child messages.
|
|
180
|
+
*
|
|
181
|
+
* Pi remains canonical. Normal finalized messages are folded once; startup,
|
|
182
|
+
* compaction, and terminal settlement replace this state from Pi's messages.
|
|
183
|
+
*/
|
|
184
|
+
export class AgentProgressProjection {
|
|
185
|
+
private usage = emptyUsage();
|
|
186
|
+
private preview = "";
|
|
187
|
+
private latestAssistant?: ProgressAssistantMessage;
|
|
188
|
+
private firstEntry?: ProjectedTranscriptEntry;
|
|
189
|
+
private tailEntries: ProjectedTranscriptEntry[] = [];
|
|
190
|
+
private totalEntries = 0;
|
|
191
|
+
private contextTokens: ContextTokens;
|
|
192
|
+
|
|
193
|
+
constructor(
|
|
194
|
+
messages: readonly AgentMessage[] = [],
|
|
195
|
+
contextTokens?: ContextTokens,
|
|
196
|
+
) {
|
|
197
|
+
this.replace(messages, contextTokens);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
replace(messages: readonly AgentMessage[], contextTokens?: ContextTokens) {
|
|
201
|
+
this.usage = emptyUsage();
|
|
202
|
+
this.preview = "";
|
|
203
|
+
this.latestAssistant = undefined;
|
|
204
|
+
this.firstEntry = undefined;
|
|
205
|
+
this.tailEntries = [];
|
|
206
|
+
this.totalEntries = 0;
|
|
207
|
+
this.contextTokens = contextTokens;
|
|
208
|
+
for (const message of messages) this.ingest(message, false);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
append(message: AgentMessage) {
|
|
212
|
+
this.ingest(message, true);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private ingest(message: AgentMessage, updateContext: boolean) {
|
|
216
|
+
if (message.role === "assistant") {
|
|
217
|
+
addAssistantUsage(this.usage, message);
|
|
218
|
+
this.latestAssistant = message;
|
|
219
|
+
const text = assistantText(message);
|
|
220
|
+
if (text) this.preview = text;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (updateContext && this.contextTokens !== undefined) {
|
|
224
|
+
const assistantTokens =
|
|
225
|
+
message.role === "assistant"
|
|
226
|
+
? validAssistantContextTokens(message)
|
|
227
|
+
: undefined;
|
|
228
|
+
if (assistantTokens !== undefined) {
|
|
229
|
+
this.contextTokens = assistantTokens;
|
|
230
|
+
} else if (this.contextTokens !== null) {
|
|
231
|
+
this.contextTokens += estimateTokens(message);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
for (const entry of messageEntries(message)) this.pushEntry(entry);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private pushEntry(entry: ProjectedTranscriptEntry) {
|
|
239
|
+
this.totalEntries++;
|
|
240
|
+
if (!this.firstEntry) {
|
|
241
|
+
this.firstEntry = entry;
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
this.tailEntries.push(entry);
|
|
245
|
+
if (this.tailEntries.length >= TRANSCRIPT_MAX_ENTRIES) {
|
|
246
|
+
this.tailEntries.shift();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
snapshot(
|
|
251
|
+
toolTimings: ReadonlyMap<string, ProgressToolTiming> = new Map(),
|
|
252
|
+
): AgentProgressProjectionSnapshot {
|
|
253
|
+
const selected = this.firstEntry
|
|
254
|
+
? [this.firstEntry, ...this.tailEntries]
|
|
255
|
+
: [];
|
|
256
|
+
const transcript: TranscriptEntry[] = [];
|
|
257
|
+
let totalBytes = 0;
|
|
258
|
+
for (const entry of selected) {
|
|
259
|
+
const remaining = TRANSCRIPT_TOTAL_MAX_BYTES - totalBytes;
|
|
260
|
+
if (remaining <= 0) break;
|
|
261
|
+
const text = truncateUtf8(
|
|
262
|
+
entry.text,
|
|
263
|
+
Math.min(TRANSCRIPT_ENTRY_MAX_BYTES, remaining),
|
|
264
|
+
);
|
|
265
|
+
totalBytes += Buffer.byteLength(text, "utf8");
|
|
266
|
+
const truncated = entry.sourceTextTruncated || text !== entry.text;
|
|
267
|
+
transcript.push({
|
|
268
|
+
role: entry.role,
|
|
269
|
+
text: truncated ? `${text}\n[transcript entry truncated]` : text,
|
|
270
|
+
...(entry.name === undefined ? {} : { name: entry.name }),
|
|
271
|
+
...(entry.sourceToolCallId === undefined
|
|
272
|
+
? {}
|
|
273
|
+
: toolMetadata(entry.sourceToolCallId, toolTimings)),
|
|
274
|
+
...(entry.isError === undefined ? {} : { isError: entry.isError }),
|
|
275
|
+
...(entry.timestamp === undefined
|
|
276
|
+
? {}
|
|
277
|
+
: { timestamp: entry.timestamp }),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
if (transcript.length < this.totalEntries) {
|
|
281
|
+
transcript.push({
|
|
282
|
+
role: "toolResult",
|
|
283
|
+
name: "transcript",
|
|
284
|
+
text: `[transcript truncated: retained ${transcript.length} of ${this.totalEntries} entries]`,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
preview: this.preview,
|
|
289
|
+
usage: {
|
|
290
|
+
...this.usage,
|
|
291
|
+
...(typeof this.contextTokens === "number"
|
|
292
|
+
? { contextTokens: this.contextTokens }
|
|
293
|
+
: {}),
|
|
294
|
+
},
|
|
295
|
+
latestAssistant: this.latestAssistant,
|
|
296
|
+
transcript,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function transcriptFromMessages(
|
|
302
|
+
messages: AgentMessage[],
|
|
303
|
+
toolTimings: ReadonlyMap<string, ProgressToolTiming> = new Map(),
|
|
304
|
+
) {
|
|
305
|
+
return new AgentProgressProjection(messages).snapshot(toolTimings).transcript;
|
|
306
|
+
}
|