pi-crew 0.2.3 → 0.2.5
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/AGENTS.md +57 -32
- package/CHANGELOG.md +466 -448
- package/LICENSE +21 -21
- package/NOTICE.md +16 -16
- package/README.md +323 -323
- package/docs/FEATURE_INTAKE.md +126 -0
- package/docs/HARNESS.md +86 -0
- package/docs/HARNESS_BACKLOG.md +41 -0
- package/docs/TEST_MATRIX.md +49 -0
- package/docs/actions-reference.md +595 -595
- package/docs/architecture.md +180 -180
- package/docs/code-review-2026-05-11.md +592 -592
- package/docs/commands-reference.md +347 -347
- package/docs/comparison-pi-subagents-vs-pi-crew.md +303 -0
- package/docs/decisions/0001-durable-state.md +41 -0
- package/docs/decisions/0002-child-process-for-async.md +42 -0
- package/docs/decisions/0003-depth-guard.md +36 -0
- package/docs/decisions/0004-execfile-over-exec.md +34 -0
- package/docs/decisions/0005-no-parameter-properties.md +49 -0
- package/docs/decisions/0006-publish-bundled-esm.md +63 -0
- package/docs/decisions/0007-active-run-binary-index.md +54 -0
- package/docs/decisions/0008-child-pi-warm-pool.md +61 -0
- package/docs/decisions/README.md +23 -0
- package/docs/followup-review-round4-2026-05-13.md +107 -0
- package/docs/implementation-plan-top3.md +333 -0
- package/docs/live-mailbox-runtime.md +36 -36
- package/docs/next-upgrade-roadmap.md +808 -808
- package/docs/oh-my-pi-research.md +509 -0
- package/docs/perf/baseline-2026-05.md +113 -0
- package/docs/perf/final-report-2026-05.md +206 -0
- package/docs/perf/sprint-1-report.md +71 -0
- package/docs/perf/sprint-2-report.md +81 -0
- package/docs/perf/sprint-2.5-report.md +53 -0
- package/docs/perf/sprint-3-report.md +36 -0
- package/docs/perf/sprint-4-report.md +47 -0
- package/docs/perf/sprint-5-report.md +51 -0
- package/docs/perf/sprint-6-report.md +94 -0
- package/docs/perf/sprint-7-report.md +74 -0
- package/docs/perf/upgrade-plan-2026-05.md +147 -0
- package/docs/pi-subagents3-deep-analysis.md +508 -0
- package/docs/product/README.md +31 -0
- package/docs/product/platform.md +27 -0
- package/docs/product/runtime-safety.md +37 -0
- package/docs/product/team-run.md +39 -0
- package/docs/product/team-tool.md +37 -0
- package/docs/publishing.md +65 -65
- package/docs/resource-formats.md +134 -134
- package/docs/runtime-analysis-child-vs-live.md +171 -0
- package/docs/runtime-flow.md +148 -148
- package/docs/runtime-migration-in-process-analysis.md +250 -0
- package/docs/stories/README.md +30 -0
- package/docs/stories/backlog.md +36 -0
- package/docs/templates/decision.md +27 -0
- package/docs/templates/story.md +44 -0
- package/docs/templates/validation-report.md +32 -0
- package/docs/usage.md +238 -238
- package/index.ts +7 -6
- package/install.mjs +65 -65
- package/package.json +107 -100
- package/schema.json +222 -222
- package/skills/child-pi-spawning/SKILL.md +213 -0
- package/skills/context-artifact-hygiene/SKILL.md +32 -0
- package/skills/event-log-tracing/SKILL.md +299 -0
- package/skills/git-master/SKILL.md +225 -24
- package/skills/live-agent-lifecycle/SKILL.md +192 -0
- package/skills/mailbox-interactive/SKILL.md +300 -19
- package/skills/model-routing-context/SKILL.md +94 -0
- package/skills/multi-perspective-review/SKILL.md +88 -0
- package/skills/read-only-explorer/SKILL.md +250 -26
- package/skills/safe-bash/SKILL.md +307 -21
- package/skills/verification-before-done/SKILL.md +11 -2
- package/skills/widget-rendering/SKILL.md +258 -0
- package/skills/workspace-isolation/SKILL.md +202 -0
- package/skills/worktree-isolation/SKILL.md +202 -18
- package/src/adapters/claude-adapter.ts +25 -25
- package/src/adapters/codex-adapter.ts +21 -21
- package/src/adapters/cursor-adapter.ts +17 -17
- package/src/adapters/export-util.ts +137 -137
- package/src/adapters/index.ts +15 -15
- package/src/adapters/registry.ts +18 -18
- package/src/adapters/types.ts +23 -23
- package/src/agents/agent-config.ts +38 -38
- package/src/agents/agent-serializer.ts +38 -38
- package/src/agents/discover-agents.ts +121 -118
- package/src/config/config.ts +740 -858
- package/src/config/defaults.ts +96 -96
- package/src/config/drift-detector.ts +211 -211
- package/src/config/markers.ts +327 -327
- package/src/config/resilient-parser.ts +109 -108
- package/src/config/suggestions.ts +74 -74
- package/src/config/types.ts +199 -0
- package/src/extension/async-notifier.ts +123 -89
- package/src/extension/autonomous-policy.ts +169 -169
- package/src/extension/cross-extension-rpc.ts +104 -104
- package/src/extension/help.ts +47 -47
- package/src/extension/import-index.ts +69 -69
- package/src/extension/management.ts +395 -382
- package/src/extension/notification-router.ts +116 -116
- package/src/extension/notification-sink.ts +51 -51
- package/src/extension/project-init.ts +168 -168
- package/src/extension/register.ts +859 -668
- package/src/extension/registration/artifact-cleanup.ts +15 -15
- package/src/extension/registration/command-utils.ts +54 -54
- package/src/extension/registration/commands.ts +559 -452
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-helpers.ts +102 -102
- package/src/extension/registration/subagent-tools.ts +220 -159
- package/src/extension/registration/team-tool.ts +159 -99
- package/src/extension/registration/viewers.ts +29 -0
- package/src/extension/result-watcher.ts +128 -128
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-export.ts +73 -73
- package/src/extension/run-import.ts +84 -84
- package/src/extension/run-index.ts +94 -94
- package/src/extension/run-maintenance.ts +142 -142
- package/src/extension/session-summary.ts +8 -8
- package/src/extension/team-manager-command.ts +96 -96
- package/src/extension/team-recommendation.ts +188 -188
- package/src/extension/team-tool/api.ts +5 -2
- package/src/extension/team-tool/cancel.ts +224 -209
- package/src/extension/team-tool/config-patch.ts +36 -36
- package/src/extension/team-tool/context.ts +60 -60
- package/src/extension/team-tool/doctor.ts +242 -242
- package/src/extension/team-tool/handle-settings.ts +421 -195
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +139 -139
- package/src/extension/team-tool/parallel-dispatch.ts +156 -156
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/respond.ts +112 -111
- package/src/extension/team-tool/run.ts +246 -229
- package/src/extension/team-tool/status.ts +110 -110
- package/src/extension/team-tool-types.ts +13 -13
- package/src/extension/team-tool.ts +344 -344
- package/src/extension/tool-result.ts +16 -16
- package/src/extension/validate-resources.ts +77 -77
- package/src/hooks/registry.ts +61 -61
- package/src/hooks/types.ts +40 -40
- package/src/i18n.ts +184 -184
- package/src/observability/correlation.ts +35 -35
- package/src/observability/event-to-metric.ts +68 -68
- package/src/observability/exporters/adapter.ts +30 -30
- package/src/observability/exporters/otlp-exporter.ts +106 -92
- package/src/observability/exporters/prometheus-exporter.ts +54 -54
- package/src/observability/metric-registry.ts +87 -87
- package/src/observability/metric-retention.ts +54 -54
- package/src/observability/metric-sink.ts +81 -56
- package/src/observability/metrics-primitives.ts +167 -167
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/adaptive-plan.ts +338 -0
- package/src/runtime/agent-control.ts +169 -169
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -114
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/async-runner.ts +153 -153
- package/src/runtime/attention-events.ts +28 -28
- package/src/runtime/auto-resume.ts +100 -100
- package/src/runtime/background-runner.ts +122 -89
- package/src/runtime/cancellation.ts +61 -61
- package/src/runtime/capability-inventory.ts +116 -116
- package/src/runtime/child-pi-pool.ts +68 -0
- package/src/runtime/child-pi.ts +541 -461
- package/src/runtime/code-summary.ts +247 -247
- package/src/runtime/compaction-summary.ts +271 -271
- package/src/runtime/concurrency.ts +58 -58
- package/src/runtime/crash-recovery.ts +317 -301
- package/src/runtime/crew-agent-records.ts +379 -281
- package/src/runtime/crew-agent-runtime.ts +60 -60
- package/src/runtime/cross-extension-rpc.ts +72 -0
- package/src/runtime/custom-tools/irc-tool.ts +201 -201
- package/src/runtime/custom-tools/submit-result-tool.ts +90 -90
- package/src/runtime/deadletter.ts +47 -47
- package/src/runtime/delivery-coordinator.ts +176 -176
- package/src/runtime/delta-conflict.ts +360 -360
- package/src/runtime/diagnostic-export.ts +102 -102
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/effectiveness.ts +82 -81
- package/src/runtime/errors/crew-errors.ts +166 -0
- package/src/runtime/event-stream-bridge.ts +92 -92
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +234 -106
- package/src/runtime/heartbeat-watcher.ts +145 -124
- package/src/runtime/iteration-hooks.ts +267 -267
- package/src/runtime/live-agent-control.ts +88 -88
- package/src/runtime/live-agent-manager.ts +377 -179
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +676 -600
- package/src/runtime/loop-gates.ts +129 -129
- package/src/runtime/manifest-cache.ts +263 -263
- package/src/runtime/mcp-proxy.ts +113 -113
- package/src/runtime/metric-parser.ts +40 -40
- package/src/runtime/model-fallback.ts +282 -274
- package/src/runtime/model-resolver.ts +118 -0
- package/src/runtime/output-validator.ts +187 -187
- package/src/runtime/overflow-recovery.ts +175 -175
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +156 -156
- package/src/runtime/parent-guard.ts +80 -80
- package/src/runtime/phase-progress.ts +217 -217
- package/src/runtime/pi-args.ts +165 -165
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/pi-spawn.ts +167 -167
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/post-checks.ts +125 -125
- package/src/runtime/post-exit-stdio-guard.ts +86 -86
- package/src/runtime/process-status.ts +97 -73
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +81 -81
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/run-tracker.ts +99 -0
- package/src/runtime/runtime-policy.ts +21 -0
- package/src/runtime/runtime-resolver.ts +94 -91
- package/src/runtime/scheduler.ts +294 -0
- package/src/runtime/semaphore.ts +131 -131
- package/src/runtime/sensitive-paths.ts +92 -92
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/settings-store.ts +103 -0
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/skill-instructions.ts +222 -222
- package/src/runtime/stale-reconciler.ts +198 -189
- package/src/runtime/streaming-output.ts +47 -0
- package/src/runtime/subagent-manager.ts +404 -400
- package/src/runtime/subprocess-tool-registry.ts +67 -67
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-graph-scheduler.ts +122 -122
- package/src/runtime/task-graph.ts +207 -207
- package/src/runtime/task-output-context.ts +177 -177
- package/src/runtime/task-packet.ts +93 -93
- package/src/runtime/task-quality.ts +207 -207
- package/src/runtime/task-runner/capabilities.ts +78 -78
- package/src/runtime/task-runner/live-executor.ts +131 -113
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/prompt-builder.ts +139 -139
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/run-projection.ts +103 -103
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/task-runner.ts +469 -459
- package/src/runtime/team-runner.ts +693 -945
- package/src/runtime/usage-tracker.ts +71 -0
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/runtime/workflow-state.ts +187 -187
- package/src/runtime/yield-handler.ts +190 -190
- package/src/schema/config-schema.ts +172 -168
- package/src/schema/team-tool-schema.ts +126 -126
- package/src/schema/validation-types.ts +151 -148
- package/src/skills/discover-skills.ts +67 -67
- package/src/skills/skill-templates.ts +374 -374
- package/src/state/active-run-registry.ts +227 -191
- package/src/state/artifact-store.ts +130 -129
- package/src/state/atomic-write.ts +262 -195
- package/src/state/blob-store.ts +116 -116
- package/src/state/contracts.ts +111 -111
- package/src/state/event-log-rotation.ts +161 -158
- package/src/state/event-log.ts +383 -303
- package/src/state/event-reconstructor.ts +217 -217
- package/src/state/jsonl-writer.ts +82 -82
- package/src/state/locks.ts +146 -146
- package/src/state/mailbox.ts +446 -405
- package/src/state/state-store.ts +364 -351
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +285 -285
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/discover-teams.ts +116 -116
- package/src/teams/team-config.ts +27 -27
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/agent-management-overlay.ts +144 -144
- package/src/ui/crew-widget.ts +487 -370
- package/src/ui/dashboard-panes/agents-pane.ts +109 -28
- package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
- package/src/ui/dashboard-panes/capability-pane.ts +59 -59
- package/src/ui/dashboard-panes/health-pane.ts +30 -30
- package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
- package/src/ui/dashboard-panes/progress-pane.ts +30 -30
- package/src/ui/dashboard-panes/transcript-pane.ts +10 -10
- package/src/ui/heartbeat-aggregator.ts +63 -63
- package/src/ui/keybinding-map.ts +97 -94
- package/src/ui/live-conversation-overlay.ts +152 -0
- package/src/ui/live-run-sidebar.ts +180 -180
- package/src/ui/mascot.ts +442 -442
- package/src/ui/overlays/agent-picker-overlay.ts +57 -57
- package/src/ui/overlays/confirm-overlay.ts +58 -58
- package/src/ui/overlays/mailbox-compose-overlay.ts +144 -144
- package/src/ui/overlays/mailbox-compose-preview.ts +63 -63
- package/src/ui/overlays/mailbox-detail-overlay.ts +122 -122
- package/src/ui/pi-ui-compat.ts +57 -57
- package/src/ui/powerbar-publisher.ts +221 -197
- package/src/ui/render-scheduler.ts +216 -143
- package/src/ui/run-action-dispatcher.ts +118 -118
- package/src/ui/run-dashboard.ts +526 -464
- package/src/ui/run-event-bus.ts +208 -208
- package/src/ui/run-snapshot-cache.ts +826 -777
- package/src/ui/settings-overlay.ts +721 -0
- package/src/ui/snapshot-types.ts +86 -70
- package/src/ui/theme-adapter.ts +190 -190
- package/src/ui/tool-progress-formatter.ts +89 -0
- package/src/ui/transcript-cache.ts +94 -94
- package/src/ui/transcript-viewer.ts +335 -335
- package/src/utils/conflict-detect.ts +662 -0
- package/src/utils/file-coalescer.ts +86 -86
- package/src/utils/frontmatter.ts +68 -68
- package/src/utils/fs-watch.ts +88 -31
- package/src/utils/gh-protocol.ts +479 -0
- package/src/utils/ids.ts +17 -17
- package/src/utils/incremental-reader.ts +104 -104
- package/src/utils/internal-error.ts +6 -6
- package/src/utils/names.ts +27 -27
- package/src/utils/paths.ts +102 -63
- package/src/utils/redaction.ts +44 -44
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/scan-cache.ts +136 -136
- package/src/utils/sse-parser.ts +134 -134
- package/src/utils/task-name-generator.ts +337 -337
- package/src/utils/timings.ts +33 -33
- package/src/utils/visual.ts +243 -198
- package/src/workflows/discover-workflows.ts +139 -139
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/workflows/workflow-config.ts +26 -26
- package/src/workflows/workflow-serializer.ts +32 -32
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/cleanup.ts +75 -75
- package/src/worktree/worktree-manager.ts +188 -188
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/tsconfig.json +19 -19
- package/workflows/default.workflow.md +30 -30
- package/workflows/fast-fix.workflow.md +23 -23
- package/workflows/implementation.workflow.md +43 -43
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
- package/skills/task-packet/SKILL.md +0 -28
- package/skills/verify-evidence/SKILL.md +0 -27
package/src/runtime/pi-args.ts
CHANGED
|
@@ -1,165 +1,165 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as os from "node:os";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
6
|
-
|
|
7
|
-
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
8
|
-
const PROMPT_RUNTIME_EXTENSION_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "prompt", "prompt-runtime.ts");
|
|
9
|
-
const TASK_ARG_LIMIT = 8000;
|
|
10
|
-
const DEFAULT_MAX_CREW_DEPTH = 2;
|
|
11
|
-
|
|
12
|
-
export interface BuildPiWorkerArgsInput {
|
|
13
|
-
task: string;
|
|
14
|
-
agent: AgentConfig;
|
|
15
|
-
model?: string;
|
|
16
|
-
sessionEnabled?: boolean;
|
|
17
|
-
maxDepth?: number;
|
|
18
|
-
skillPaths?: string[];
|
|
19
|
-
env?: NodeJS.ProcessEnv;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface BuildPiWorkerArgsResult {
|
|
23
|
-
args: string[];
|
|
24
|
-
env: Record<string, string | undefined>;
|
|
25
|
-
tempDir?: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function isValidThinkingLevel(value: string | undefined): value is string {
|
|
29
|
-
return value !== undefined && THINKING_LEVELS.includes(value);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function applyThinkingSuffix(model: string | undefined, thinking: string | undefined): string | undefined {
|
|
33
|
-
if (!model || !thinking || thinking === "off") return model;
|
|
34
|
-
const colonIdx = model.lastIndexOf(":");
|
|
35
|
-
if (colonIdx !== -1 && isValidThinkingLevel(model.substring(colonIdx + 1))) return model;
|
|
36
|
-
// Invalid config values fall back to Pi's default thinking behavior.
|
|
37
|
-
if (!isValidThinkingLevel(thinking)) return model;
|
|
38
|
-
return `${model}:${thinking}`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function currentCrewDepth(env: NodeJS.ProcessEnv = process.env): number {
|
|
42
|
-
const raw = env.PI_CREW_DEPTH ?? env.PI_TEAMS_DEPTH ?? "0";
|
|
43
|
-
const parsed = Number(raw);
|
|
44
|
-
return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function resolveCrewMaxDepth(inputMaxDepth?: number, env: NodeJS.ProcessEnv = process.env): number {
|
|
48
|
-
const raw = env.PI_CREW_MAX_DEPTH ?? env.PI_TEAMS_MAX_DEPTH;
|
|
49
|
-
const envDepth = raw !== undefined ? Number(raw) : NaN;
|
|
50
|
-
if (Number.isInteger(envDepth) && envDepth >= 0) return envDepth;
|
|
51
|
-
return Number.isInteger(inputMaxDepth) && inputMaxDepth !== undefined && inputMaxDepth >= 0 ? inputMaxDepth : DEFAULT_MAX_CREW_DEPTH;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function checkCrewDepth(inputMaxDepth?: number, env: NodeJS.ProcessEnv = process.env): { blocked: boolean; depth: number; maxDepth: number } {
|
|
55
|
-
const depth = currentCrewDepth(env);
|
|
56
|
-
const maxDepth = resolveCrewMaxDepth(inputMaxDepth, env);
|
|
57
|
-
return { depth, maxDepth, blocked: depth >= maxDepth };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Create a safe temp directory with symlink protection.
|
|
62
|
-
* 1. mkdtempSync to create the directory
|
|
63
|
-
* 2. lstatSync to verify it is not a symlink (TOCTOU safety)
|
|
64
|
-
* 3. realpathSync to resolve the canonical path
|
|
65
|
-
*/
|
|
66
|
-
function createSafeTempDir(base: string, prefix: string): string {
|
|
67
|
-
if (!fs.existsSync(base)) fs.mkdirSync(base, { recursive: true });
|
|
68
|
-
// Verify base dir is not a symlink (TOCTOU safety)
|
|
69
|
-
const baseStat = fs.lstatSync(base);
|
|
70
|
-
if (baseStat.isSymbolicLink()) throw new Error("Refusing to create temp dir in symlinked base: " + base);
|
|
71
|
-
// Resolve base to canonical path before joining
|
|
72
|
-
const resolvedBase = fs.realpathSync(base);
|
|
73
|
-
const rawTempDir = fs.mkdtempSync(path.join(resolvedBase, prefix));
|
|
74
|
-
try {
|
|
75
|
-
const stat = fs.lstatSync(rawTempDir);
|
|
76
|
-
if (stat.isSymbolicLink()) throw new Error("temp dir is a symlink");
|
|
77
|
-
} catch (e) {
|
|
78
|
-
if (e instanceof Error && e.message.includes("symlink")) {
|
|
79
|
-
fs.rmSync(rawTempDir, { recursive: true, force: true });
|
|
80
|
-
throw new Error("Refusing to use symlinked temp directory.");
|
|
81
|
-
}
|
|
82
|
-
throw e;
|
|
83
|
-
}
|
|
84
|
-
return fs.realpathSync(rawTempDir);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function buildPiWorkerArgs(input: BuildPiWorkerArgsInput): BuildPiWorkerArgsResult {
|
|
88
|
-
const args = ["--mode", "json", "-p"];
|
|
89
|
-
if (input.sessionEnabled === false) args.push("--no-session");
|
|
90
|
-
|
|
91
|
-
const resolvedModel = input.model ?? input.agent.model;
|
|
92
|
-
if (resolvedModel) {
|
|
93
|
-
const modelWithThinking = applyThinkingSuffix(resolvedModel, input.agent.thinking);
|
|
94
|
-
if (modelWithThinking) args.push("--model", modelWithThinking);
|
|
95
|
-
}
|
|
96
|
-
// When no model resolved, pass thinking separately so Pi can apply it to the inherited parent model.
|
|
97
|
-
if (!resolvedModel && input.agent.thinking && input.agent.thinking !== "off" && isValidThinkingLevel(input.agent.thinking)) {
|
|
98
|
-
args.push("--thinking", input.agent.thinking);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (input.agent.tools?.length) args.push("--tools", input.agent.tools.join(","));
|
|
102
|
-
if (input.agent.extensions !== undefined) {
|
|
103
|
-
args.push("--no-extensions");
|
|
104
|
-
for (const extension of [PROMPT_RUNTIME_EXTENSION_PATH, ...input.agent.extensions]) args.push("--extension", extension);
|
|
105
|
-
} else {
|
|
106
|
-
args.push("--extension", PROMPT_RUNTIME_EXTENSION_PATH);
|
|
107
|
-
}
|
|
108
|
-
if (!input.agent.inheritSkills) args.push("--no-skills");
|
|
109
|
-
for (const skillPath of input.skillPaths ?? []) args.push("--skill", skillPath);
|
|
110
|
-
|
|
111
|
-
let tempDir: string | undefined;
|
|
112
|
-
if (input.agent.systemPrompt) {
|
|
113
|
-
// On Windows, prefer a subdirectory within the user's profile over system temp
|
|
114
|
-
const tmpBase = process.platform === "win32" && os.homedir()
|
|
115
|
-
? path.join(os.homedir(), ".pi-crew", "tmp")
|
|
116
|
-
: os.tmpdir();
|
|
117
|
-
tempDir = createSafeTempDir(tmpBase, `pi-crew-${process.pid}-`);
|
|
118
|
-
const promptPath = path.join(tempDir, `${input.agent.name.replace(/[^\w.-]/g, "_")}.md`);
|
|
119
|
-
fs.writeFileSync(promptPath, input.agent.systemPrompt, { mode: 0o600 });
|
|
120
|
-
args.push(input.agent.systemPromptMode === "append" ? "--append-system-prompt" : "--system-prompt", promptPath);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (input.task.length > TASK_ARG_LIMIT) {
|
|
124
|
-
if (!tempDir) {
|
|
125
|
-
const tmpBase = process.platform === "win32" && os.homedir()
|
|
126
|
-
? path.join(os.homedir(), ".pi-crew", "tmp")
|
|
127
|
-
: os.tmpdir();
|
|
128
|
-
tempDir = createSafeTempDir(tmpBase, `pi-crew-${process.pid}-`);
|
|
129
|
-
}
|
|
130
|
-
const taskPath = path.join(tempDir, "task.md");
|
|
131
|
-
fs.writeFileSync(taskPath, input.task, { mode: 0o600 });
|
|
132
|
-
args.push(`@${taskPath}`);
|
|
133
|
-
} else {
|
|
134
|
-
args.push(`Task: ${input.task}`);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const env = input.env ?? process.env;
|
|
138
|
-
const parentDepth = currentCrewDepth(env);
|
|
139
|
-
const maxDepth = resolveCrewMaxDepth(input.maxDepth, env);
|
|
140
|
-
return {
|
|
141
|
-
args,
|
|
142
|
-
env: {
|
|
143
|
-
PI_CREW_INHERIT_PROJECT_CONTEXT: input.agent.inheritProjectContext ? "1" : "0",
|
|
144
|
-
PI_CREW_INHERIT_SKILLS: input.agent.inheritSkills ? "1" : "0",
|
|
145
|
-
PI_CREW_DEPTH: String(parentDepth + 1),
|
|
146
|
-
PI_CREW_MAX_DEPTH: String(maxDepth),
|
|
147
|
-
PI_CREW_ROLE: input.agent.name,
|
|
148
|
-
PI_TEAMS_INHERIT_PROJECT_CONTEXT: input.agent.inheritProjectContext ? "1" : "0",
|
|
149
|
-
PI_TEAMS_INHERIT_SKILLS: input.agent.inheritSkills ? "1" : "0",
|
|
150
|
-
PI_TEAMS_DEPTH: String(parentDepth + 1),
|
|
151
|
-
PI_TEAMS_MAX_DEPTH: String(maxDepth),
|
|
152
|
-
PI_TEAMS_ROLE: input.agent.name,
|
|
153
|
-
},
|
|
154
|
-
tempDir,
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function cleanupTempDir(tempDir: string | undefined): void {
|
|
159
|
-
if (!tempDir) return;
|
|
160
|
-
try {
|
|
161
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
162
|
-
} catch {
|
|
163
|
-
// Best effort.
|
|
164
|
-
}
|
|
165
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
6
|
+
|
|
7
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
8
|
+
const PROMPT_RUNTIME_EXTENSION_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "prompt", "prompt-runtime.ts");
|
|
9
|
+
const TASK_ARG_LIMIT = 8000;
|
|
10
|
+
const DEFAULT_MAX_CREW_DEPTH = 2;
|
|
11
|
+
|
|
12
|
+
export interface BuildPiWorkerArgsInput {
|
|
13
|
+
task: string;
|
|
14
|
+
agent: AgentConfig;
|
|
15
|
+
model?: string;
|
|
16
|
+
sessionEnabled?: boolean;
|
|
17
|
+
maxDepth?: number;
|
|
18
|
+
skillPaths?: string[];
|
|
19
|
+
env?: NodeJS.ProcessEnv;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface BuildPiWorkerArgsResult {
|
|
23
|
+
args: string[];
|
|
24
|
+
env: Record<string, string | undefined>;
|
|
25
|
+
tempDir?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isValidThinkingLevel(value: string | undefined): value is string {
|
|
29
|
+
return value !== undefined && THINKING_LEVELS.includes(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function applyThinkingSuffix(model: string | undefined, thinking: string | undefined): string | undefined {
|
|
33
|
+
if (!model || !thinking || thinking === "off") return model;
|
|
34
|
+
const colonIdx = model.lastIndexOf(":");
|
|
35
|
+
if (colonIdx !== -1 && isValidThinkingLevel(model.substring(colonIdx + 1))) return model;
|
|
36
|
+
// Invalid config values fall back to Pi's default thinking behavior.
|
|
37
|
+
if (!isValidThinkingLevel(thinking)) return model;
|
|
38
|
+
return `${model}:${thinking}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function currentCrewDepth(env: NodeJS.ProcessEnv = process.env): number {
|
|
42
|
+
const raw = env.PI_CREW_DEPTH ?? env.PI_TEAMS_DEPTH ?? "0";
|
|
43
|
+
const parsed = Number(raw);
|
|
44
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function resolveCrewMaxDepth(inputMaxDepth?: number, env: NodeJS.ProcessEnv = process.env): number {
|
|
48
|
+
const raw = env.PI_CREW_MAX_DEPTH ?? env.PI_TEAMS_MAX_DEPTH;
|
|
49
|
+
const envDepth = raw !== undefined ? Number(raw) : NaN;
|
|
50
|
+
if (Number.isInteger(envDepth) && envDepth >= 0) return envDepth;
|
|
51
|
+
return Number.isInteger(inputMaxDepth) && inputMaxDepth !== undefined && inputMaxDepth >= 0 ? inputMaxDepth : DEFAULT_MAX_CREW_DEPTH;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function checkCrewDepth(inputMaxDepth?: number, env: NodeJS.ProcessEnv = process.env): { blocked: boolean; depth: number; maxDepth: number } {
|
|
55
|
+
const depth = currentCrewDepth(env);
|
|
56
|
+
const maxDepth = resolveCrewMaxDepth(inputMaxDepth, env);
|
|
57
|
+
return { depth, maxDepth, blocked: depth >= maxDepth };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create a safe temp directory with symlink protection.
|
|
62
|
+
* 1. mkdtempSync to create the directory
|
|
63
|
+
* 2. lstatSync to verify it is not a symlink (TOCTOU safety)
|
|
64
|
+
* 3. realpathSync to resolve the canonical path
|
|
65
|
+
*/
|
|
66
|
+
function createSafeTempDir(base: string, prefix: string): string {
|
|
67
|
+
if (!fs.existsSync(base)) fs.mkdirSync(base, { recursive: true });
|
|
68
|
+
// Verify base dir is not a symlink (TOCTOU safety)
|
|
69
|
+
const baseStat = fs.lstatSync(base);
|
|
70
|
+
if (baseStat.isSymbolicLink()) throw new Error("Refusing to create temp dir in symlinked base: " + base);
|
|
71
|
+
// Resolve base to canonical path before joining
|
|
72
|
+
const resolvedBase = fs.realpathSync(base);
|
|
73
|
+
const rawTempDir = fs.mkdtempSync(path.join(resolvedBase, prefix));
|
|
74
|
+
try {
|
|
75
|
+
const stat = fs.lstatSync(rawTempDir);
|
|
76
|
+
if (stat.isSymbolicLink()) throw new Error("temp dir is a symlink");
|
|
77
|
+
} catch (e) {
|
|
78
|
+
if (e instanceof Error && e.message.includes("symlink")) {
|
|
79
|
+
fs.rmSync(rawTempDir, { recursive: true, force: true });
|
|
80
|
+
throw new Error("Refusing to use symlinked temp directory.");
|
|
81
|
+
}
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
return fs.realpathSync(rawTempDir);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function buildPiWorkerArgs(input: BuildPiWorkerArgsInput): BuildPiWorkerArgsResult {
|
|
88
|
+
const args = ["--mode", "json", "-p"];
|
|
89
|
+
if (input.sessionEnabled === false) args.push("--no-session");
|
|
90
|
+
|
|
91
|
+
const resolvedModel = input.model ?? input.agent.model;
|
|
92
|
+
if (resolvedModel) {
|
|
93
|
+
const modelWithThinking = applyThinkingSuffix(resolvedModel, input.agent.thinking);
|
|
94
|
+
if (modelWithThinking) args.push("--model", modelWithThinking);
|
|
95
|
+
}
|
|
96
|
+
// When no model resolved, pass thinking separately so Pi can apply it to the inherited parent model.
|
|
97
|
+
if (!resolvedModel && input.agent.thinking && input.agent.thinking !== "off" && isValidThinkingLevel(input.agent.thinking)) {
|
|
98
|
+
args.push("--thinking", input.agent.thinking);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (input.agent.tools?.length) args.push("--tools", input.agent.tools.join(","));
|
|
102
|
+
if (input.agent.extensions !== undefined) {
|
|
103
|
+
args.push("--no-extensions");
|
|
104
|
+
for (const extension of [PROMPT_RUNTIME_EXTENSION_PATH, ...input.agent.extensions]) args.push("--extension", extension);
|
|
105
|
+
} else {
|
|
106
|
+
args.push("--extension", PROMPT_RUNTIME_EXTENSION_PATH);
|
|
107
|
+
}
|
|
108
|
+
if (!input.agent.inheritSkills) args.push("--no-skills");
|
|
109
|
+
for (const skillPath of input.skillPaths ?? []) args.push("--skill", skillPath);
|
|
110
|
+
|
|
111
|
+
let tempDir: string | undefined;
|
|
112
|
+
if (input.agent.systemPrompt) {
|
|
113
|
+
// On Windows, prefer a subdirectory within the user's profile over system temp
|
|
114
|
+
const tmpBase = process.platform === "win32" && os.homedir()
|
|
115
|
+
? path.join(os.homedir(), ".pi-crew", "tmp")
|
|
116
|
+
: os.tmpdir();
|
|
117
|
+
tempDir = createSafeTempDir(tmpBase, `pi-crew-${process.pid}-`);
|
|
118
|
+
const promptPath = path.join(tempDir, `${input.agent.name.replace(/[^\w.-]/g, "_")}.md`);
|
|
119
|
+
fs.writeFileSync(promptPath, input.agent.systemPrompt, { mode: 0o600 });
|
|
120
|
+
args.push(input.agent.systemPromptMode === "append" ? "--append-system-prompt" : "--system-prompt", promptPath);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (input.task.length > TASK_ARG_LIMIT) {
|
|
124
|
+
if (!tempDir) {
|
|
125
|
+
const tmpBase = process.platform === "win32" && os.homedir()
|
|
126
|
+
? path.join(os.homedir(), ".pi-crew", "tmp")
|
|
127
|
+
: os.tmpdir();
|
|
128
|
+
tempDir = createSafeTempDir(tmpBase, `pi-crew-${process.pid}-`);
|
|
129
|
+
}
|
|
130
|
+
const taskPath = path.join(tempDir, "task.md");
|
|
131
|
+
fs.writeFileSync(taskPath, input.task, { mode: 0o600 });
|
|
132
|
+
args.push(`@${taskPath}`);
|
|
133
|
+
} else {
|
|
134
|
+
args.push(`Task: ${input.task}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const env = input.env ?? process.env;
|
|
138
|
+
const parentDepth = currentCrewDepth(env);
|
|
139
|
+
const maxDepth = resolveCrewMaxDepth(input.maxDepth, env);
|
|
140
|
+
return {
|
|
141
|
+
args,
|
|
142
|
+
env: {
|
|
143
|
+
PI_CREW_INHERIT_PROJECT_CONTEXT: input.agent.inheritProjectContext ? "1" : "0",
|
|
144
|
+
PI_CREW_INHERIT_SKILLS: input.agent.inheritSkills ? "1" : "0",
|
|
145
|
+
PI_CREW_DEPTH: String(parentDepth + 1),
|
|
146
|
+
PI_CREW_MAX_DEPTH: String(maxDepth),
|
|
147
|
+
PI_CREW_ROLE: input.agent.name,
|
|
148
|
+
PI_TEAMS_INHERIT_PROJECT_CONTEXT: input.agent.inheritProjectContext ? "1" : "0",
|
|
149
|
+
PI_TEAMS_INHERIT_SKILLS: input.agent.inheritSkills ? "1" : "0",
|
|
150
|
+
PI_TEAMS_DEPTH: String(parentDepth + 1),
|
|
151
|
+
PI_TEAMS_MAX_DEPTH: String(maxDepth),
|
|
152
|
+
PI_TEAMS_ROLE: input.agent.name,
|
|
153
|
+
},
|
|
154
|
+
tempDir,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function cleanupTempDir(tempDir: string | undefined): void {
|
|
159
|
+
if (!tempDir) return;
|
|
160
|
+
try {
|
|
161
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
162
|
+
} catch {
|
|
163
|
+
// Best effort.
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -1,111 +1,111 @@
|
|
|
1
|
-
export interface ParsedPiUsage {
|
|
2
|
-
input?: number;
|
|
3
|
-
output?: number;
|
|
4
|
-
cacheRead?: number;
|
|
5
|
-
cacheWrite?: number;
|
|
6
|
-
cost?: number;
|
|
7
|
-
turns?: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface ParsedPiJsonOutput {
|
|
11
|
-
jsonEvents: number;
|
|
12
|
-
textEvents: string[];
|
|
13
|
-
finalText?: string;
|
|
14
|
-
usage?: ParsedPiUsage;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
18
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function numberField(obj: Record<string, unknown>, keys: string[]): number | undefined {
|
|
22
|
-
for (const key of keys) {
|
|
23
|
-
const value = obj[key];
|
|
24
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
25
|
-
}
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function mergeUsage(target: ParsedPiUsage, source: ParsedPiUsage): ParsedPiUsage {
|
|
30
|
-
return {
|
|
31
|
-
input: source.input ?? target.input,
|
|
32
|
-
output: source.output ?? target.output,
|
|
33
|
-
cacheRead: source.cacheRead ?? target.cacheRead,
|
|
34
|
-
cacheWrite: source.cacheWrite ?? target.cacheWrite,
|
|
35
|
-
cost: source.cost ?? target.cost,
|
|
36
|
-
turns: source.turns ?? target.turns,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function extractUsage(value: unknown): ParsedPiUsage | undefined {
|
|
41
|
-
const obj = asRecord(value);
|
|
42
|
-
if (!obj) return undefined;
|
|
43
|
-
const direct: ParsedPiUsage = {
|
|
44
|
-
input: numberField(obj, ["input", "inputTokens", "input_tokens"]),
|
|
45
|
-
output: numberField(obj, ["output", "outputTokens", "output_tokens"]),
|
|
46
|
-
cacheRead: numberField(obj, ["cacheRead", "cache_read", "cacheReadTokens", "cache_read_tokens"]),
|
|
47
|
-
cacheWrite: numberField(obj, ["cacheWrite", "cache_write", "cacheWriteTokens", "cache_write_tokens"]),
|
|
48
|
-
cost: numberField(obj, ["cost", "costUsd", "cost_usd"]),
|
|
49
|
-
turns: numberField(obj, ["turns", "turnCount", "turn_count"]),
|
|
50
|
-
};
|
|
51
|
-
if (Object.values(direct).some((entry) => entry !== undefined)) return direct;
|
|
52
|
-
for (const key of ["usage", "tokenUsage", "tokens", "stats"]) {
|
|
53
|
-
const nested = extractUsage(obj[key]);
|
|
54
|
-
if (nested) return nested;
|
|
55
|
-
}
|
|
56
|
-
return undefined;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function textFromContent(content: unknown): string[] {
|
|
60
|
-
if (typeof content === "string") return [content];
|
|
61
|
-
if (!Array.isArray(content)) return [];
|
|
62
|
-
const text: string[] = [];
|
|
63
|
-
for (const part of content) {
|
|
64
|
-
const obj = asRecord(part);
|
|
65
|
-
if (!obj) continue;
|
|
66
|
-
if (obj.type === "text" && typeof obj.text === "string") text.push(obj.text);
|
|
67
|
-
else if (typeof obj.content === "string") text.push(obj.content);
|
|
68
|
-
}
|
|
69
|
-
return text;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function extractText(value: unknown): string[] {
|
|
73
|
-
const obj = asRecord(value);
|
|
74
|
-
if (!obj) return [];
|
|
75
|
-
const message = asRecord(obj.message);
|
|
76
|
-
if (message?.role !== undefined && message.role !== "assistant") return [];
|
|
77
|
-
const text: string[] = [];
|
|
78
|
-
if (typeof obj.text === "string") text.push(obj.text);
|
|
79
|
-
if (typeof obj.output === "string") text.push(obj.output);
|
|
80
|
-
if (typeof obj.finalOutput === "string") text.push(obj.finalOutput);
|
|
81
|
-
if (typeof obj.final_output === "string") text.push(obj.final_output);
|
|
82
|
-
if (!message) text.push(...textFromContent(obj.content));
|
|
83
|
-
if (message) text.push(...textFromContent(message.content));
|
|
84
|
-
return text.filter((entry) => entry.trim().length > 0);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function parsePiJsonOutput(stdout: string): ParsedPiJsonOutput {
|
|
88
|
-
let jsonEvents = 0;
|
|
89
|
-
const textEvents: string[] = [];
|
|
90
|
-
let usage: ParsedPiUsage | undefined;
|
|
91
|
-
for (const line of stdout.split("\n")) {
|
|
92
|
-
const trimmed = line.trim();
|
|
93
|
-
if (!trimmed) continue;
|
|
94
|
-
let event: unknown;
|
|
95
|
-
try {
|
|
96
|
-
event = JSON.parse(trimmed) as unknown;
|
|
97
|
-
} catch {
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
jsonEvents++;
|
|
101
|
-
textEvents.push(...extractText(event));
|
|
102
|
-
const eventUsage = extractUsage(event);
|
|
103
|
-
if (eventUsage) usage = mergeUsage(usage ?? {}, eventUsage);
|
|
104
|
-
}
|
|
105
|
-
return {
|
|
106
|
-
jsonEvents,
|
|
107
|
-
textEvents,
|
|
108
|
-
finalText: textEvents.length > 0 ? textEvents[textEvents.length - 1] : undefined,
|
|
109
|
-
usage,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
1
|
+
export interface ParsedPiUsage {
|
|
2
|
+
input?: number;
|
|
3
|
+
output?: number;
|
|
4
|
+
cacheRead?: number;
|
|
5
|
+
cacheWrite?: number;
|
|
6
|
+
cost?: number;
|
|
7
|
+
turns?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ParsedPiJsonOutput {
|
|
11
|
+
jsonEvents: number;
|
|
12
|
+
textEvents: string[];
|
|
13
|
+
finalText?: string;
|
|
14
|
+
usage?: ParsedPiUsage;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
18
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function numberField(obj: Record<string, unknown>, keys: string[]): number | undefined {
|
|
22
|
+
for (const key of keys) {
|
|
23
|
+
const value = obj[key];
|
|
24
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function mergeUsage(target: ParsedPiUsage, source: ParsedPiUsage): ParsedPiUsage {
|
|
30
|
+
return {
|
|
31
|
+
input: source.input ?? target.input,
|
|
32
|
+
output: source.output ?? target.output,
|
|
33
|
+
cacheRead: source.cacheRead ?? target.cacheRead,
|
|
34
|
+
cacheWrite: source.cacheWrite ?? target.cacheWrite,
|
|
35
|
+
cost: source.cost ?? target.cost,
|
|
36
|
+
turns: source.turns ?? target.turns,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function extractUsage(value: unknown): ParsedPiUsage | undefined {
|
|
41
|
+
const obj = asRecord(value);
|
|
42
|
+
if (!obj) return undefined;
|
|
43
|
+
const direct: ParsedPiUsage = {
|
|
44
|
+
input: numberField(obj, ["input", "inputTokens", "input_tokens"]),
|
|
45
|
+
output: numberField(obj, ["output", "outputTokens", "output_tokens"]),
|
|
46
|
+
cacheRead: numberField(obj, ["cacheRead", "cache_read", "cacheReadTokens", "cache_read_tokens"]),
|
|
47
|
+
cacheWrite: numberField(obj, ["cacheWrite", "cache_write", "cacheWriteTokens", "cache_write_tokens"]),
|
|
48
|
+
cost: numberField(obj, ["cost", "costUsd", "cost_usd"]),
|
|
49
|
+
turns: numberField(obj, ["turns", "turnCount", "turn_count"]),
|
|
50
|
+
};
|
|
51
|
+
if (Object.values(direct).some((entry) => entry !== undefined)) return direct;
|
|
52
|
+
for (const key of ["usage", "tokenUsage", "tokens", "stats"]) {
|
|
53
|
+
const nested = extractUsage(obj[key]);
|
|
54
|
+
if (nested) return nested;
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function textFromContent(content: unknown): string[] {
|
|
60
|
+
if (typeof content === "string") return [content];
|
|
61
|
+
if (!Array.isArray(content)) return [];
|
|
62
|
+
const text: string[] = [];
|
|
63
|
+
for (const part of content) {
|
|
64
|
+
const obj = asRecord(part);
|
|
65
|
+
if (!obj) continue;
|
|
66
|
+
if (obj.type === "text" && typeof obj.text === "string") text.push(obj.text);
|
|
67
|
+
else if (typeof obj.content === "string") text.push(obj.content);
|
|
68
|
+
}
|
|
69
|
+
return text;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function extractText(value: unknown): string[] {
|
|
73
|
+
const obj = asRecord(value);
|
|
74
|
+
if (!obj) return [];
|
|
75
|
+
const message = asRecord(obj.message);
|
|
76
|
+
if (message?.role !== undefined && message.role !== "assistant") return [];
|
|
77
|
+
const text: string[] = [];
|
|
78
|
+
if (typeof obj.text === "string") text.push(obj.text);
|
|
79
|
+
if (typeof obj.output === "string") text.push(obj.output);
|
|
80
|
+
if (typeof obj.finalOutput === "string") text.push(obj.finalOutput);
|
|
81
|
+
if (typeof obj.final_output === "string") text.push(obj.final_output);
|
|
82
|
+
if (!message) text.push(...textFromContent(obj.content));
|
|
83
|
+
if (message) text.push(...textFromContent(message.content));
|
|
84
|
+
return text.filter((entry) => entry.trim().length > 0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function parsePiJsonOutput(stdout: string): ParsedPiJsonOutput {
|
|
88
|
+
let jsonEvents = 0;
|
|
89
|
+
const textEvents: string[] = [];
|
|
90
|
+
let usage: ParsedPiUsage | undefined;
|
|
91
|
+
for (const line of stdout.split("\n")) {
|
|
92
|
+
const trimmed = line.trim();
|
|
93
|
+
if (!trimmed) continue;
|
|
94
|
+
let event: unknown;
|
|
95
|
+
try {
|
|
96
|
+
event = JSON.parse(trimmed) as unknown;
|
|
97
|
+
} catch {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
jsonEvents++;
|
|
101
|
+
textEvents.push(...extractText(event));
|
|
102
|
+
const eventUsage = extractUsage(event);
|
|
103
|
+
if (eventUsage) usage = mergeUsage(usage ?? {}, eventUsage);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
jsonEvents,
|
|
107
|
+
textEvents,
|
|
108
|
+
finalText: textEvents.length > 0 ? textEvents[textEvents.length - 1] : undefined,
|
|
109
|
+
usage,
|
|
110
|
+
};
|
|
111
|
+
}
|