pi-crew 0.9.68 → 0.10.2
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 +222 -0
- package/NOTICE.md +21 -0
- package/README.md +44 -2
- package/agents/analyst.md +1 -1
- package/agents/cold-verifier.md +3 -1
- package/agents/critic.md +1 -1
- package/agents/executor.md +1 -1
- package/agents/explorer.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/agents/writer.md +1 -1
- package/dist/index.mjs +68113 -60774
- package/docs/README.md +2 -0
- package/docs/actions-reference.md +31 -0
- package/docs/commands-reference.md +17 -6
- package/docs/resource-formats.md +13 -0
- package/package.json +4 -2
- package/schema.json +503 -91
- package/scripts/resource-sampler.mjs +36 -2
- package/skills/requirements-to-task-packet/SKILL.md +26 -0
- package/skills/widget-rendering/SKILL.md +7 -7
- package/src/agents/agent-config.ts +2 -1
- package/src/agents/discover-agents.ts +23 -14
- package/src/config/config-merge.ts +183 -0
- package/src/config/config-validation.ts +687 -0
- package/src/config/config.ts +22 -864
- package/src/config/defaults.ts +43 -2
- package/src/config/drift-detector.ts +1 -1
- package/src/config/env-vars.ts +691 -0
- package/src/config/role-tools.ts +11 -9
- package/src/config/sanitize-project-config.ts +172 -0
- package/src/config/types.ts +49 -1
- package/src/extension/async-notifier.ts +25 -2
- package/src/extension/crew-cleanup.ts +13 -0
- package/src/extension/crew-vibes/config.ts +2 -1
- package/src/extension/crew-vibes/footer.ts +19 -0
- package/src/extension/crew-vibes/index.ts +11 -1
- package/src/extension/plan-orchestrate.ts +132 -0
- package/src/extension/register.ts +8 -0
- package/src/extension/registration/command-registration.ts +1 -0
- package/src/extension/registration/commands/dashboard.ts +158 -0
- package/src/extension/registration/commands/index.ts +35 -0
- package/src/extension/registration/commands/manage.ts +303 -0
- package/src/extension/registration/commands/run.ts +228 -0
- package/src/extension/registration/commands/shared.ts +639 -0
- package/src/extension/registration/commands/status.ts +60 -0
- package/src/extension/registration/commands.ts +13 -1224
- package/src/extension/registration/foreground-run-controller.ts +10 -2
- package/src/extension/registration/lifecycle-handlers.ts +178 -17
- package/src/extension/registration/runtime-cleanup.ts +23 -5
- package/src/extension/registration/subagent-tools.ts +218 -9
- package/src/extension/registration/team-tool.ts +5 -1
- package/src/extension/registration/ui.ts +5 -4
- package/src/extension/rpc-hmac.ts +5 -3
- package/src/extension/team-tool/api/heartbeat.ts +47 -10
- package/src/extension/team-tool/api/plan-approval.ts +9 -0
- package/src/extension/team-tool/api/task-claims.ts +109 -40
- package/src/extension/team-tool/cancel.ts +84 -50
- package/src/extension/team-tool/dispatch/index.ts +1 -0
- package/src/extension/team-tool/dispatch/run.ts +4 -1
- package/src/extension/team-tool/doctor.ts +103 -1
- package/src/extension/team-tool/orchestrate.ts +66 -1
- package/src/extension/team-tool/plans.ts +192 -0
- package/src/extension/team-tool/respond.ts +197 -65
- package/src/extension/team-tool/run-deadline.ts +35 -3
- package/src/extension/team-tool/run-intent.ts +63 -0
- package/src/extension/team-tool/run.ts +74 -20
- package/src/extension/team-tool/status.ts +84 -26
- package/src/extension/team-tool.ts +11 -2
- package/src/hooks/registry.ts +1 -6
- package/src/i18n.ts +9 -0
- package/src/prompt/prompt-runtime.ts +521 -2
- package/src/prompt/worker-events-channel.ts +173 -0
- package/src/runtime/README.md +8 -8
- package/src/runtime/async-runner.ts +7 -3
- package/src/runtime/background-runner.ts +42 -14
- package/src/runtime/broker/broker-issuer.ts +9 -2
- package/src/runtime/broker/crew-broker-tokens.ts +43 -6
- package/src/runtime/broker/crew-broker.ts +838 -10
- package/src/runtime/broker/wait-status-cache.ts +157 -0
- package/src/runtime/budget-enforcement.ts +281 -0
- package/src/runtime/child-pi/child-pi-constants.ts +8 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
- package/src/runtime/child-pi/child-pi-streams.ts +21 -1
- package/src/runtime/child-pi/child-pi-timers.ts +324 -0
- package/src/runtime/child-pi/child-pi.ts +97 -201
- package/src/runtime/child-pi/mock-fixtures.ts +16 -2
- package/src/runtime/crew-agent-records.ts +259 -14
- package/src/runtime/delegate-spawn.ts +148 -0
- package/src/runtime/detached-run-results.ts +90 -0
- package/src/runtime/deterministic-ast.ts +2 -1
- package/src/runtime/dispatch-batch.ts +945 -0
- package/src/runtime/finalize-run.ts +557 -0
- package/src/runtime/goal-workflow/adaptive-plan.ts +116 -15
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +8 -3
- package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
- package/src/runtime/group-join.ts +11 -125
- package/src/runtime/live-session/live-session-runtime.ts +26 -1
- package/src/runtime/merge-gate.ts +32 -10
- package/src/runtime/merge-loop.ts +130 -0
- package/src/runtime/model/model-budget-summary.ts +53 -0
- package/src/runtime/model/model-fallback.ts +36 -2
- package/src/runtime/model/pi-args.ts +10 -0
- package/src/runtime/model/provider-extensions.ts +10 -0
- package/src/runtime/orphan-worker-registry.ts +1 -1
- package/src/runtime/output/output-validator.ts +45 -0
- package/src/runtime/parent-guard.ts +3 -1
- package/src/runtime/peer-dep.ts +2 -1
- package/src/runtime/per-write-validator.ts +0 -5
- package/src/runtime/pi-spawn.ts +61 -15
- package/src/runtime/plan-approval.ts +125 -0
- package/src/runtime/plan-replan.ts +151 -0
- package/src/runtime/process-status.ts +16 -1
- package/src/runtime/recovery/checkpoint.ts +0 -18
- package/src/runtime/recovery/crash-recovery.ts +111 -46
- package/src/runtime/run-tracker.ts +77 -10
- package/src/runtime/scheduler-context.ts +98 -0
- package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
- package/src/runtime/scheduling/global-worker-cap.ts +2 -1
- package/src/runtime/scheduling/nested-slots.ts +70 -0
- package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
- package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
- package/src/runtime/settings-store.ts +219 -0
- package/src/runtime/spawn-policy.ts +217 -0
- package/src/runtime/stale-reconciler.ts +87 -6
- package/src/runtime/subagent-manager.ts +25 -1
- package/src/runtime/task-output-context.ts +230 -9
- package/src/runtime/task-packet.ts +23 -1
- package/src/runtime/task-runner/child-executor.ts +106 -7
- package/src/runtime/task-runner/post-execution.ts +125 -1
- package/src/runtime/task-runner/pre-execution.ts +39 -1
- package/src/runtime/task-runner/prompt-builder.ts +51 -1
- package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
- package/src/runtime/task-runner/spec-evidence.ts +403 -0
- package/src/runtime/task-runner/state-helpers.ts +26 -24
- package/src/runtime/task-runner.ts +11 -0
- package/src/runtime/team-runner.ts +132 -1673
- package/src/runtime/verification/spec-sandbox.ts +255 -0
- package/src/runtime/verification/verification-gates.ts +3 -2
- package/src/runtime/verification/verification-worktree.ts +2 -1
- package/src/runtime/workflow-phase-advance.ts +100 -0
- package/src/runtime/workspace-tree.ts +9 -0
- package/src/schema/config-schema.ts +66 -26
- package/src/schema/sensitive-config-paths.ts +64 -0
- package/src/schema/team-tool-schema.ts +13 -3
- package/src/state/README.md +4 -10
- package/src/state/atomic-write.ts +20 -3
- package/src/state/contracts.ts +38 -0
- package/src/state/coordination/mailbox.ts +12 -2
- package/src/state/event-log/cursor.ts +223 -0
- package/src/state/event-log/event-log-rotation.ts +12 -4
- package/src/state/event-log/event-log.ts +152 -369
- package/src/state/event-log/sequence-cache.ts +373 -0
- package/src/state/event-log/worker-atomic-writer.ts +2 -1
- package/src/state/stores/active-run-registry.ts +3 -2
- package/src/state/stores/manifest-io.ts +237 -0
- package/src/state/stores/ownership-map.ts +162 -0
- package/src/state/stores/plan-store.ts +241 -0
- package/src/state/stores/run-cache.ts +0 -90
- package/src/state/stores/spec-store.ts +189 -0
- package/src/state/stores/state-store.ts +139 -232
- package/src/state/types.ts +199 -0
- package/src/ui/dashboard-panes/plan-pane.ts +136 -0
- package/src/ui/dashboard-panes/progress-pane.ts +6 -0
- package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
- package/src/ui/dock-footer.ts +49 -0
- package/src/ui/heartbeat-aggregator.ts +9 -1
- package/src/ui/inline-panel/agent-pane.ts +375 -0
- package/src/ui/inline-panel/agent-transcript.ts +338 -0
- package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
- package/src/ui/inline-panel/crew-editor.ts +192 -0
- package/src/ui/inline-panel/index.ts +290 -0
- package/src/ui/inline-panel/panel-rows.ts +37 -0
- package/src/ui/inline-panel/panel-selection.ts +157 -0
- package/src/ui/inline-panel/panel-store.ts +111 -0
- package/src/ui/inline-panel/view-session-store.ts +36 -0
- package/src/ui/keybinding-map.ts +54 -13
- package/src/ui/pi-ui-compat.ts +9 -0
- package/src/ui/powerbar-publisher.ts +52 -1
- package/src/ui/run-dashboard.ts +31 -5
- package/src/ui/run-snapshot-cache.ts +57 -30
- package/src/ui/snapshot-types.ts +6 -1
- package/src/ui/widget/index.ts +176 -22
- package/src/ui/widget/task-list.ts +198 -0
- package/src/ui/widget/widget-formatters.ts +240 -4
- package/src/ui/widget/widget-renderer.ts +243 -38
- package/src/ui/widget/widget-types.ts +11 -0
- package/src/utils/child-process-shield.ts +106 -0
- package/src/utils/file-coalescer.ts +0 -4
- package/src/utils/fs-errno.ts +66 -0
- package/src/utils/fs-watch.ts +1 -1
- package/src/utils/internal-error.ts +3 -1
- package/src/utils/paths.ts +11 -3
- package/src/utils/redaction.ts +7 -0
- package/src/utils/safe-abort.ts +45 -0
- package/src/utils/task-name-generator.ts +1 -8
- package/src/workflows/discover-workflows.ts +20 -2
- package/src/workflows/validate-workflow.ts +7 -1
- package/src/workflows/workflow-config.ts +17 -0
- package/src/workflows/workflow-serializer.ts +3 -0
- package/src/worktree/worktree-manager.ts +22 -0
- package/workflows/default.workflow.md +36 -26
- package/workflows/strict-fast-fix.workflow.md +26 -0
- package/src/agents/agent-search.ts +0 -98
- package/src/benchmark/benchmark-runner.ts +0 -313
- package/src/benchmark/feedback-loop.ts +0 -73
- package/src/config/resilient-parser.ts +0 -117
- package/src/extension/crew-vibes/cat-frames.ts +0 -18
- package/src/extension/result-watcher.ts +0 -139
- package/src/observability/exporters/prometheus-exporter.ts +0 -54
- package/src/observability/metric-retention.ts +0 -64
- package/src/runtime/compaction/compaction-summary.ts +0 -278
- package/src/runtime/errors/crew-errors.ts +0 -162
- package/src/runtime/live-session/intercom-bridge.ts +0 -187
- package/src/runtime/loop-gates.ts +0 -128
- package/src/runtime/metric-parser.ts +0 -36
- package/src/runtime/output/stream-preview.ts +0 -184
- package/src/runtime/output/tool-progress.ts +0 -278
- package/src/runtime/phase-tracker.ts +0 -385
- package/src/runtime/pipeline-runner.ts +0 -523
- package/src/runtime/process/process-lifecycle.ts +0 -491
- package/src/runtime/recovery/retry-runner.ts +0 -330
- package/src/runtime/run-drift.ts +0 -219
- package/src/runtime/task-quality.ts +0 -199
- package/src/runtime/task-runner/run-projection.ts +0 -128
- package/src/runtime/verification/post-checks.ts +0 -142
- package/src/state/coordination/schedule.ts +0 -166
- package/src/state/event-log/jsonl-writer.ts +0 -115
- package/src/state/hook-instinct-bridge.ts +0 -94
- package/src/state/hook-integrations.ts +0 -51
- package/src/state/session-state-map.ts +0 -51
- package/src/state/stores/blob-store.ts +0 -308
- package/src/state/stores/instinct-store.ts +0 -275
- package/src/state/stores/observation-store.ts +0 -176
- package/src/state/tiered-eval.ts +0 -480
- package/src/state/types-eval.ts +0 -58
- package/src/tools/safe-bash-extension.ts +0 -54
- package/src/tools/safe-bash.ts +0 -505
- package/src/ui/agent-management-overlay.ts +0 -160
- package/src/ui/crew-footer.ts +0 -102
- package/src/ui/crew-select-list.ts +0 -114
- package/src/ui/dashboard-panes/capability-pane.ts +0 -77
- package/src/ui/transcript-entries.ts +0 -256
- package/src/utils/conflict-detect.ts +0 -721
- package/src/utils/fingerprint.ts +0 -180
- package/src/utils/gh-protocol.ts +0 -556
- package/src/utils/project-detector.ts +0 -160
- package/src/utils/sse-parser.ts +0 -131
- package/src/workflows/cost-estimator.ts +0 -34
- package/src/workflows/intermediate-store.ts +0 -166
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-transcript.ts — per-agent event JSONL → typed transcript items.
|
|
3
|
+
*
|
|
4
|
+
* The source of truth is the on-disk per-agent event log
|
|
5
|
+
* (`<stateRoot>/agents/<taskId>/events.jsonl`), written by
|
|
6
|
+
* `appendCrewAgentEventBuffered` from the single `onJsonEvent` funnel in
|
|
7
|
+
* `child-executor.ts`. Events are already compacted upstream
|
|
8
|
+
* (`child-pi-streams.ts:52-101`), so each record is small and shaped for
|
|
9
|
+
* display.
|
|
10
|
+
*
|
|
11
|
+
* Disk is chosen over a pure in-memory feed because child-executor only lives
|
|
12
|
+
* in the extension process for **foreground** runs; async runs execute in a
|
|
13
|
+
* detached spawner, so a memory feed would show an empty pane for exactly the
|
|
14
|
+
* runs users background. Disk works for both with one parser.
|
|
15
|
+
*
|
|
16
|
+
* Per task, a module-level ring buffer accumulates parsed items across reads
|
|
17
|
+
* (cursor + persisted parse state), mirroring pi-subtask's `fork.transcript`.
|
|
18
|
+
* This matters for two reasons the naive delta-read gets wrong:
|
|
19
|
+
* - tool `start`/`end` events can straddle a read boundary, so the
|
|
20
|
+
* unmatched-start map must survive between reads;
|
|
21
|
+
* - the pane renders the FULL recent history on every tick, not just the
|
|
22
|
+
* new tail.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import * as fs from "node:fs";
|
|
26
|
+
import * as path from "node:path";
|
|
27
|
+
|
|
28
|
+
import { readCrewAgentEventsCursor } from "../../runtime/crew-agent-records.ts";
|
|
29
|
+
import type { TeamRunManifest } from "../../state/types.ts";
|
|
30
|
+
|
|
31
|
+
const MAX_TRANSCRIPT_ITEMS = 500;
|
|
32
|
+
|
|
33
|
+
/** Normalize a raw usage record to pi's shape (footer/dashboard consumers
|
|
34
|
+
* read usage.input / usage.cost.total unconditionally). */
|
|
35
|
+
export function normalizeUsage(raw: unknown): {
|
|
36
|
+
input: number;
|
|
37
|
+
output: number;
|
|
38
|
+
cacheRead: number;
|
|
39
|
+
cacheWrite: number;
|
|
40
|
+
cost: { total: number };
|
|
41
|
+
} {
|
|
42
|
+
const usage = raw && typeof raw === "object" && !Array.isArray(raw) ? (raw as Record<string, unknown>) : undefined;
|
|
43
|
+
const toNum = (value: unknown): number => (typeof value === "number" && Number.isFinite(value) ? value : 0);
|
|
44
|
+
const costRaw = usage ? usage.cost : undefined;
|
|
45
|
+
const costRecord = costRaw && typeof costRaw === "object" && !Array.isArray(costRaw) ? (costRaw as Record<string, unknown>) : undefined;
|
|
46
|
+
return {
|
|
47
|
+
input: toNum(usage?.input),
|
|
48
|
+
output: toNum(usage?.output),
|
|
49
|
+
cacheRead: toNum(usage?.cacheRead),
|
|
50
|
+
cacheWrite: toNum(usage?.cacheWrite),
|
|
51
|
+
cost: { total: toNum(costRecord?.total ?? costRaw) },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type CrewTranscriptItem =
|
|
56
|
+
| { type: "user"; text: string; seq: number }
|
|
57
|
+
| {
|
|
58
|
+
type: "assistant";
|
|
59
|
+
text: string;
|
|
60
|
+
seq: number;
|
|
61
|
+
/** The compacted assistant message (content/usage/model/stopReason),
|
|
62
|
+
* retained so the pane can render with pi's own
|
|
63
|
+
* AssistantMessageComponent instead of plain markdown. Usage is
|
|
64
|
+
* normalized to pi's shape (footer/dashboard consumers read
|
|
65
|
+
* usage.input / usage.cost.total unconditionally). */
|
|
66
|
+
message?: Record<string, unknown>;
|
|
67
|
+
/** Usage normalized for the pane's own footer line (parallels the
|
|
68
|
+
* view-session builder; absent when the event carried none). */
|
|
69
|
+
usage?: ReturnType<typeof normalizeUsage>;
|
|
70
|
+
}
|
|
71
|
+
| {
|
|
72
|
+
type: "tool";
|
|
73
|
+
name: string;
|
|
74
|
+
toolCallId: string;
|
|
75
|
+
args: Record<string, unknown>;
|
|
76
|
+
/** Normalized to ToolExecutionComponent.updateResult's shape:
|
|
77
|
+
* `{ content: parts[], isError }` (pi's own tool-result envelope). */
|
|
78
|
+
result?: { content: Array<{ type: string; text?: string }>; isError: boolean };
|
|
79
|
+
isError?: boolean;
|
|
80
|
+
seq: number;
|
|
81
|
+
}
|
|
82
|
+
| { type: "system"; text: string; seq: number };
|
|
83
|
+
|
|
84
|
+
type PendingTool = CrewTranscriptItem & { type: "tool" };
|
|
85
|
+
|
|
86
|
+
/** Per-task parse state, persisted across reads. */
|
|
87
|
+
const buffers = new Map<string, CrewTranscriptItem[]>();
|
|
88
|
+
const pendingByTask = new Map<string, Map<string, PendingTool>>();
|
|
89
|
+
const cursors = new Map<string, number>();
|
|
90
|
+
/** Tasks whose worker prompt has been prepended to the buffer. */
|
|
91
|
+
const promptSeeded = new Set<string>();
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The child pi never logs its INITIAL user message (input is not an event),
|
|
95
|
+
* so the transcript would open on the first assistant message — unlike a
|
|
96
|
+
* real session. The full worker prompt is persisted at
|
|
97
|
+
* `artifacts/{runId}/prompts/{taskId}.md`; seed it as the opening user item
|
|
98
|
+
* for session parity. Returns undefined while the artifact has not been
|
|
99
|
+
* written yet (retried on the next read).
|
|
100
|
+
*/
|
|
101
|
+
function readWorkerPrompt(manifest: TeamRunManifest, taskId: string): string | undefined {
|
|
102
|
+
try {
|
|
103
|
+
const file = path.join(manifest.artifactsRoot, "prompts", `${taskId}.md`);
|
|
104
|
+
const text = fs.readFileSync(file, "utf-8").trim();
|
|
105
|
+
return text || undefined;
|
|
106
|
+
} catch {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
112
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
113
|
+
return value as Record<string, unknown>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Normalize a tool result's content into ToolExecutionComponent.updateResult's
|
|
118
|
+
* envelope. The log holds either shape depending on age/source: a plain
|
|
119
|
+
* string (compacted toolResult part) or an array of content parts
|
|
120
|
+
* (role:"toolResult" message_end).
|
|
121
|
+
*/
|
|
122
|
+
function normalizeResultContent(raw: unknown): Array<{ type: string; text?: string }> {
|
|
123
|
+
if (typeof raw === "string") return [{ type: "text", text: raw }];
|
|
124
|
+
if (!Array.isArray(raw)) return [];
|
|
125
|
+
return raw.flatMap((part) => {
|
|
126
|
+
if (typeof part === "string") return [{ type: "text", text: part }];
|
|
127
|
+
const record = asRecord(part);
|
|
128
|
+
if (!record) return [];
|
|
129
|
+
return [
|
|
130
|
+
{
|
|
131
|
+
type: typeof record.type === "string" ? record.type : "text",
|
|
132
|
+
text: typeof record.text === "string" ? record.text : undefined,
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function textFromContent(content: unknown): string {
|
|
139
|
+
if (!Array.isArray(content)) return "";
|
|
140
|
+
return content
|
|
141
|
+
.flatMap((part) => {
|
|
142
|
+
const item = asRecord(part);
|
|
143
|
+
if (!item) return [];
|
|
144
|
+
if (item.type === "text" && typeof item.text === "string") return [item.text];
|
|
145
|
+
return [];
|
|
146
|
+
})
|
|
147
|
+
.join("\n")
|
|
148
|
+
.trim();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Parse compacted event records into display items.
|
|
153
|
+
*
|
|
154
|
+
* The compaction in `child-pi-streams.ts` drops `toolCallId` from tool events,
|
|
155
|
+
* so `tool_execution_start` and its result are paired by tool name in arrival
|
|
156
|
+
* order — the nearest unmatched start with the same name. This is correct for
|
|
157
|
+
* sequential tool use; concurrent same-name tools are rare and the worst case
|
|
158
|
+
* is a result landing on the wrong start, not a crash.
|
|
159
|
+
*
|
|
160
|
+
* `message_end` events carry the full compacted `Message`. Assistant messages
|
|
161
|
+
* produce text items; tool cards come from `tool_execution_start` events.
|
|
162
|
+
* Results arrive either as role:"toolResult" message_end records (pi ≥0.84,
|
|
163
|
+
* no tool name — folded FIFO) or as toolResult parts inside older assistant
|
|
164
|
+
* messages (named — folded by name).
|
|
165
|
+
*/
|
|
166
|
+
function parseEventRecord(record: Record<string, unknown>, pending: Map<string, PendingTool>): CrewTranscriptItem[] {
|
|
167
|
+
const seq = typeof record.seq === "number" ? record.seq : 0;
|
|
168
|
+
const event = asRecord(record.event) ?? record;
|
|
169
|
+
const type = typeof event.type === "string" ? event.type : "";
|
|
170
|
+
|
|
171
|
+
const items: CrewTranscriptItem[] = [];
|
|
172
|
+
|
|
173
|
+
if (type === "tool_execution_start") {
|
|
174
|
+
const name = typeof event.toolName === "string" ? event.toolName : "tool";
|
|
175
|
+
const args = (event.args as Record<string, unknown>) ?? {};
|
|
176
|
+
const id = `${name}#${seq}`;
|
|
177
|
+
const item: PendingTool = { type: "tool", name, toolCallId: id, args, seq };
|
|
178
|
+
pending.set(id, item);
|
|
179
|
+
items.push(item);
|
|
180
|
+
return items;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (type === "tool_execution_end") {
|
|
184
|
+
// The compacted end event carries NO result (child-pi-streams.ts keeps
|
|
185
|
+
// only type/toolName/args). The actual result arrives as the next
|
|
186
|
+
// message_end's toolResult content part, so the pending start must stay
|
|
187
|
+
// in the map until that fold — removing it here would leave the card
|
|
188
|
+
// permanently stuck in "started, no result". Nothing to emit.
|
|
189
|
+
return items;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (type === "message_end" || type === "message" || type === "tool_result_end") {
|
|
193
|
+
const message = asRecord(event.message);
|
|
194
|
+
if (!message) return items;
|
|
195
|
+
|
|
196
|
+
if (message.role === "toolResult") {
|
|
197
|
+
// pi ≥0.84 emits each tool result as its OWN message_end (role
|
|
198
|
+
// "toolResult", content parts, NO tool name) instead of toolResult
|
|
199
|
+
// parts inside the assistant message. Results arrive in the
|
|
200
|
+
// originating message's toolCall order, and starts are pushed in
|
|
201
|
+
// that same order, so fold FIFO into the oldest pending start that
|
|
202
|
+
// has no result yet — correct for concurrent tools too.
|
|
203
|
+
const result = {
|
|
204
|
+
content: normalizeResultContent(message.content),
|
|
205
|
+
isError: message.isError === true,
|
|
206
|
+
};
|
|
207
|
+
for (const [id, item] of pending) {
|
|
208
|
+
if (item.result !== undefined) continue;
|
|
209
|
+
item.result = result;
|
|
210
|
+
item.isError = result.isError;
|
|
211
|
+
pending.delete(id);
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
return items;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (message.role === "assistant") {
|
|
218
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
219
|
+
const text = textFromContent(content);
|
|
220
|
+
if (text) {
|
|
221
|
+
// Compaction can carry usage at the RECORD level (usage-only
|
|
222
|
+
// tail) instead of inside the message — merge it in so the
|
|
223
|
+
// pane's full-message render and usage footer see it.
|
|
224
|
+
const recordUsage = asRecord(event.usage);
|
|
225
|
+
const messageUsage = asRecord(message.usage);
|
|
226
|
+
let merged = message;
|
|
227
|
+
if (recordUsage) {
|
|
228
|
+
merged = { ...message, usage: messageUsage ? { ...messageUsage, ...recordUsage } : recordUsage };
|
|
229
|
+
}
|
|
230
|
+
items.push({ type: "assistant", text, seq, message: merged, usage: normalizeUsage(merged.usage) });
|
|
231
|
+
}
|
|
232
|
+
// toolResult parts carry name + content; fold them into pending starts
|
|
233
|
+
// (normalized to the same updateResult envelope as the pi ≥0.84
|
|
234
|
+
// role:"toolResult" messages above).
|
|
235
|
+
for (const part of content) {
|
|
236
|
+
const item = asRecord(part);
|
|
237
|
+
if (item?.type !== "toolResult") continue;
|
|
238
|
+
const name = typeof item.name === "string" ? item.name : "tool";
|
|
239
|
+
const isError = item.isError === true;
|
|
240
|
+
matchPending(
|
|
241
|
+
pending,
|
|
242
|
+
name,
|
|
243
|
+
{
|
|
244
|
+
content: normalizeResultContent(item.content),
|
|
245
|
+
isError,
|
|
246
|
+
},
|
|
247
|
+
isError,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return items;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (message.role === "user") {
|
|
254
|
+
const text = textFromContent(message.content);
|
|
255
|
+
if (text) items.push({ type: "user", text, seq });
|
|
256
|
+
return items;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// System / other events become a dim system line.
|
|
261
|
+
if (type && type !== "message_update") {
|
|
262
|
+
const text = typeof event.text === "string" ? event.text : "";
|
|
263
|
+
if (text) items.push({ type: "system", text, seq });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return items;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function matchPending(
|
|
270
|
+
pending: Map<string, PendingTool>,
|
|
271
|
+
name: string,
|
|
272
|
+
result: { content: Array<{ type: string; text?: string }>; isError: boolean },
|
|
273
|
+
isError: boolean | undefined,
|
|
274
|
+
): void {
|
|
275
|
+
for (const [id, item] of [...pending.entries()].reverse()) {
|
|
276
|
+
if (item.name !== name) continue;
|
|
277
|
+
pending.delete(id);
|
|
278
|
+
if (result !== undefined) item.result = result;
|
|
279
|
+
if (isError !== undefined) item.isError = isError;
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Read new events and return the accumulated transcript (most recent
|
|
286
|
+
* MAX_TRANSCRIPT_ITEMS, chronological). Cheap when nothing new arrived: the
|
|
287
|
+
* cursor skips re-parse and the existing buffer is returned as-is.
|
|
288
|
+
*/
|
|
289
|
+
export function readAgentTranscript(manifest: TeamRunManifest, taskId: string): CrewTranscriptItem[] {
|
|
290
|
+
const sinceSeq = cursors.get(taskId) ?? 0;
|
|
291
|
+
const { events, nextSeq } = readCrewAgentEventsCursor(manifest, taskId, { sinceSeq });
|
|
292
|
+
if (nextSeq > sinceSeq) cursors.set(taskId, nextSeq);
|
|
293
|
+
|
|
294
|
+
let buffer = buffers.get(taskId) ?? [];
|
|
295
|
+
if (events.length > 0) {
|
|
296
|
+
const pending = pendingByTask.get(taskId) ?? new Map<string, PendingTool>();
|
|
297
|
+
pendingByTask.set(taskId, pending);
|
|
298
|
+
|
|
299
|
+
for (const record of events) {
|
|
300
|
+
const parsed = asRecord(record);
|
|
301
|
+
if (!parsed) continue;
|
|
302
|
+
buffer.push(...parseEventRecord(parsed, pending));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (buffer.length > 0 && !promptSeeded.has(taskId)) {
|
|
306
|
+
const prompt = readWorkerPrompt(manifest, taskId);
|
|
307
|
+
if (prompt !== undefined) {
|
|
308
|
+
promptSeeded.add(taskId);
|
|
309
|
+
buffer.unshift({ type: "user", text: prompt, seq: 0 });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (buffer.length > MAX_TRANSCRIPT_ITEMS) {
|
|
313
|
+
buffer = buffer.slice(buffer.length - MAX_TRANSCRIPT_ITEMS);
|
|
314
|
+
}
|
|
315
|
+
buffers.set(taskId, buffer);
|
|
316
|
+
return buffer;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Drop everything learned about a task (used when the pane switches agents). */
|
|
320
|
+
export function resetAgentTranscriptCursor(taskId: string): void {
|
|
321
|
+
cursors.delete(taskId);
|
|
322
|
+
buffers.delete(taskId);
|
|
323
|
+
pendingByTask.delete(taskId);
|
|
324
|
+
promptSeeded.delete(taskId);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Clear all per-task state (session teardown / test isolation). */
|
|
328
|
+
export function resetAllAgentTranscriptCursors(): void {
|
|
329
|
+
cursors.clear();
|
|
330
|
+
buffers.clear();
|
|
331
|
+
pendingByTask.clear();
|
|
332
|
+
promptSeeded.clear();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Test-only: whether the module currently holds state for a task. */
|
|
336
|
+
export function __hasAgentTranscriptState(taskId: string): boolean {
|
|
337
|
+
return buffers.has(taskId) || cursors.has(taskId) || pendingByTask.has(taskId);
|
|
338
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-view-overlay.ts — the agent view as a FULL-SCREEN, separate surface.
|
|
3
|
+
*
|
|
4
|
+
* Opened through `ctx.ui.custom(..., { overlay: true })` with width "100%"
|
|
5
|
+
* and zero margins, so viewing an agent TAKES OVER the terminal instead of
|
|
6
|
+
* flowing under the main transcript: what the user sees is that agent's live
|
|
7
|
+
* session and nothing else. The main session keeps running underneath (its
|
|
8
|
+
* editor, widgets and run state are untouched) and comes back the moment the
|
|
9
|
+
* overlay closes — no session is switched, resumed, or torn down, so a view
|
|
10
|
+
* can never kill a run.
|
|
11
|
+
*
|
|
12
|
+
* Keyboard (the overlay captures focus while open):
|
|
13
|
+
* esc / q / ctrl+c close, back to the main conversation
|
|
14
|
+
* pgup / pgdn / ↑ ↓ / j k / g G scroll the transcript
|
|
15
|
+
* tab / shift+tab switch to the next / previous agent in place
|
|
16
|
+
* i steer input — enter sends to the viewed agent, esc cancels
|
|
17
|
+
*
|
|
18
|
+
* The transcript itself is `CrewAgentPane` (shared with the widget-mode
|
|
19
|
+
* view): pi's own message/tool components, disk-tailed live, per-assistant
|
|
20
|
+
* usage footers — session parity, full height.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
24
|
+
import { matchesKey, type TUI, truncateToWidth } from "@earendil-works/pi-tui";
|
|
25
|
+
import { asCrewTheme, type CrewTheme } from "../theme-adapter.ts";
|
|
26
|
+
import { CrewAgentPane } from "./agent-pane.ts";
|
|
27
|
+
import type { PanelTarget } from "./panel-selection.ts";
|
|
28
|
+
import { getViewedAgent, panelRows, setViewedAgent } from "./panel-store.ts";
|
|
29
|
+
|
|
30
|
+
export interface CrewAgentOverlayOptions {
|
|
31
|
+
/** Close the view and restore the main conversation. */
|
|
32
|
+
close(): void;
|
|
33
|
+
/** Deliver a steer message to the viewed agent. */
|
|
34
|
+
steer(target: PanelTarget, message: string): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const INPUT_PROMPT = " ❯ ";
|
|
38
|
+
|
|
39
|
+
/** Printable single keypress or paste chunk: no control bytes, no ESC
|
|
40
|
+
* sequences (arrow keys etc. must not land in the steer text). */
|
|
41
|
+
function isPrintableInput(data: string): boolean {
|
|
42
|
+
return data.length > 0 && !data.startsWith("\x1b") && !/[\x00-\x1f\x7f]/.test(data);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class CrewAgentOverlay {
|
|
46
|
+
/** The transcript core; exposed so the host can wire scroll/steer keys. */
|
|
47
|
+
readonly pane: CrewAgentPane;
|
|
48
|
+
|
|
49
|
+
private disposed = false;
|
|
50
|
+
private inputMode = false;
|
|
51
|
+
private inputText = "";
|
|
52
|
+
private closed = false;
|
|
53
|
+
private tui: TUI;
|
|
54
|
+
private theme: CrewTheme;
|
|
55
|
+
private readonly options: CrewAgentOverlayOptions;
|
|
56
|
+
|
|
57
|
+
constructor(tui: TUI, theme: Theme, cwd: string, options: CrewAgentOverlayOptions) {
|
|
58
|
+
this.tui = tui;
|
|
59
|
+
this.theme = asCrewTheme(theme);
|
|
60
|
+
this.options = options;
|
|
61
|
+
this.pane = new CrewAgentPane(tui, theme, cwd, {
|
|
62
|
+
// Fill the terminal: overlay chrome (hint + optional input row)
|
|
63
|
+
// plus the pane's own header/border/indicator lines.
|
|
64
|
+
maxBodyLines: (headerLines) => this.availableBodyLines(headerLines),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Idempotent: the escape key and the host's close path share this. */
|
|
69
|
+
requestClose(): void {
|
|
70
|
+
if (this.closed) return;
|
|
71
|
+
this.closed = true;
|
|
72
|
+
this.options.close();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
requestRender(): void {
|
|
76
|
+
if (!this.disposed) this.tui.requestRender();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private chromeLines(): number {
|
|
80
|
+
return 1 /* hint */ + (this.inputMode ? 1 /* input row */ : 0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private availableBodyLines(headerLines: number): number {
|
|
84
|
+
const rows = this.tui.terminal.rows;
|
|
85
|
+
// pane self-chrome: border + spacer/"more" indicator (+1 slack for the
|
|
86
|
+
// bottom "↓ more" indicator that appears while scrolled).
|
|
87
|
+
return Math.max(4, rows - headerLines - 4 - this.chromeLines());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private hintLine(width: number): string {
|
|
91
|
+
if (this.inputMode) {
|
|
92
|
+
return this.theme.fg("dim", truncateToWidth(" enter send · esc cancel steer", width, "…"));
|
|
93
|
+
}
|
|
94
|
+
const viewed = getViewedAgent();
|
|
95
|
+
const rows = panelRows();
|
|
96
|
+
const row = viewed ? rows.find((r) => r.runId === viewed.runId && r.taskId === viewed.taskId) : undefined;
|
|
97
|
+
const label = row?.name ? `@${row.name.replace(/\s+/g, " ")}` : "agent";
|
|
98
|
+
const multi = rows.length > 1 ? " · tab agent" : "";
|
|
99
|
+
return this.theme.fg("dim", truncateToWidth(` viewing ${label}${multi} · pgup/pgdn scroll · i steer · esc back`, width, "…"));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private inputLine(width: number): string {
|
|
103
|
+
const text = `${INPUT_PROMPT}${this.inputText}▏`;
|
|
104
|
+
return this.theme.fg("accent", truncateToWidth(text, width, "…"));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Switch the view to the next (delta=+1) or previous (delta=-1) agent. */
|
|
108
|
+
private cycleAgent(delta: 1 | -1): void {
|
|
109
|
+
const rows = panelRows();
|
|
110
|
+
if (rows.length === 0) return;
|
|
111
|
+
const viewed = getViewedAgent();
|
|
112
|
+
const index = viewed ? rows.findIndex((r) => r.runId === viewed.runId && r.taskId === viewed.taskId) : -1;
|
|
113
|
+
const next = rows[(((index + delta) % rows.length) + rows.length) % rows.length];
|
|
114
|
+
if (!next) return;
|
|
115
|
+
if (viewed && next.runId === viewed.runId && next.taskId === viewed.taskId) return;
|
|
116
|
+
setViewedAgent({ runId: next.runId, taskId: next.taskId });
|
|
117
|
+
this.pane.requestRender();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private sendSteer(): void {
|
|
121
|
+
const text = this.inputText.trim();
|
|
122
|
+
const viewed = getViewedAgent();
|
|
123
|
+
this.inputMode = false;
|
|
124
|
+
this.inputText = "";
|
|
125
|
+
if (!text || !viewed) return;
|
|
126
|
+
this.options.steer(viewed, text);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
handleInput(data: string): void {
|
|
130
|
+
if (this.disposed || this.closed) return;
|
|
131
|
+
|
|
132
|
+
if (this.inputMode) {
|
|
133
|
+
if (matchesKey(data, "escape")) {
|
|
134
|
+
this.inputMode = false;
|
|
135
|
+
this.inputText = "";
|
|
136
|
+
} else if (matchesKey(data, "return")) {
|
|
137
|
+
this.sendSteer();
|
|
138
|
+
} else if (matchesKey(data, "backspace")) {
|
|
139
|
+
this.inputText = this.inputText.slice(0, -1);
|
|
140
|
+
} else if (data === "\x03") {
|
|
141
|
+
// ctrl+c inside steer input cancels the input, not the view.
|
|
142
|
+
this.inputMode = false;
|
|
143
|
+
this.inputText = "";
|
|
144
|
+
} else if (isPrintableInput(data)) {
|
|
145
|
+
// Printable (including multi-byte UTF-8 and bracketed paste).
|
|
146
|
+
this.inputText += data;
|
|
147
|
+
}
|
|
148
|
+
this.tui.requestRender();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (matchesKey(data, "escape") || data === "\x03") {
|
|
153
|
+
this.requestClose();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (data === "q") {
|
|
157
|
+
this.requestClose();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (matchesKey(data, "pageUp")) {
|
|
161
|
+
this.pane.scrollBy(10);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (matchesKey(data, "pageDown")) {
|
|
165
|
+
this.pane.scrollBy(-10);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (matchesKey(data, "up") || data === "k") {
|
|
169
|
+
this.pane.scrollBy(1);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (matchesKey(data, "down") || data === "j") {
|
|
173
|
+
this.pane.scrollBy(-1);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (data === "g" || matchesKey(data, "home")) {
|
|
177
|
+
this.pane.scrollHome();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (data === "G" || matchesKey(data, "end")) {
|
|
181
|
+
this.pane.scrollEnd();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (matchesKey(data, "tab")) {
|
|
185
|
+
this.cycleAgent(1);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (matchesKey(data, "shift+tab")) {
|
|
189
|
+
this.cycleAgent(-1);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (data === "i") {
|
|
193
|
+
this.inputMode = true;
|
|
194
|
+
this.inputText = "";
|
|
195
|
+
this.tui.requestRender();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// Everything else (incl. kitty release events) is ignored — the view
|
|
199
|
+
// never leaks keys into the main editor underneath.
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
render(width: number): string[] {
|
|
203
|
+
if (this.disposed || this.closed) return [];
|
|
204
|
+
|
|
205
|
+
const rows = this.tui.terminal.rows;
|
|
206
|
+
const lines = this.pane.render(width);
|
|
207
|
+
// Pad so the overlay always covers the full terminal — the session
|
|
208
|
+
// underneath must not bleed through on short transcripts.
|
|
209
|
+
const bodyBudget = rows - this.chromeLines();
|
|
210
|
+
while (lines.length < bodyBudget) lines.push("");
|
|
211
|
+
if (lines.length > bodyBudget) lines.length = bodyBudget;
|
|
212
|
+
if (this.inputMode) lines.push(this.inputLine(width));
|
|
213
|
+
lines.push(this.hintLine(width));
|
|
214
|
+
return lines.slice(0, Math.max(1, rows));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
invalidate(): void {
|
|
218
|
+
this.pane.invalidate();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
dispose(): void {
|
|
222
|
+
this.disposed = true;
|
|
223
|
+
this.pane.dispose();
|
|
224
|
+
}
|
|
225
|
+
}
|