pi-crew 0.1.45 → 0.1.49
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 +97 -0
- package/README.md +5 -5
- package/agents/analyst.md +11 -11
- package/agents/critic.md +11 -11
- package/agents/executor.md +11 -11
- package/agents/explorer.md +11 -11
- package/agents/planner.md +11 -11
- package/agents/reviewer.md +11 -11
- package/agents/security-reviewer.md +11 -11
- package/agents/test-engineer.md +11 -11
- package/agents/verifier.md +11 -11
- package/agents/writer.md +11 -11
- package/docs/next-upgrade-roadmap.md +808 -0
- package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +261 -0
- package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +111 -0
- package/docs/research/AUDIT_OH_MY_PI.md +261 -0
- package/docs/research/AUDIT_PI_CREW.md +457 -0
- package/docs/research/CAVEMAN-DEEP-RESEARCH.md +281 -0
- package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +264 -0
- package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +343 -0
- package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +480 -0
- package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +354 -0
- package/docs/research/IMPLEMENTATION_PLAN.md +385 -0
- package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +502 -0
- package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +266 -0
- package/docs/research/REMAINING-GAPS-PLAN.md +363 -0
- package/docs/research/SESSION-SUMMARY-2026-05-08.md +146 -0
- package/docs/research/UI-RESPONSIVENESS-AUDIT.md +173 -0
- package/docs/research-awesome-agent-skills-distillation.md +100 -0
- package/docs/research-oh-my-pi-distillation.md +369 -0
- package/docs/source-runtime-refactor-map.md +24 -0
- package/docs/usage.md +3 -3
- package/install.mjs +52 -8
- package/package.json +99 -98
- package/schema.json +10 -1
- package/skills/async-worker-recovery/SKILL.md +42 -0
- package/skills/context-artifact-hygiene/SKILL.md +52 -0
- package/skills/delegation-patterns/SKILL.md +54 -0
- package/skills/mailbox-interactive/SKILL.md +40 -0
- package/skills/model-routing-context/SKILL.md +39 -0
- package/skills/multi-perspective-review/SKILL.md +58 -0
- package/skills/observability-reliability/SKILL.md +41 -0
- package/skills/orchestration/SKILL.md +157 -0
- package/skills/ownership-session-security/SKILL.md +41 -0
- package/skills/pi-extension-lifecycle/SKILL.md +39 -0
- package/skills/requirements-to-task-packet/SKILL.md +63 -0
- package/skills/resource-discovery-config/SKILL.md +41 -0
- package/skills/runtime-state-reader/SKILL.md +44 -0
- package/skills/secure-agent-orchestration-review/SKILL.md +45 -0
- package/skills/state-mutation-locking/SKILL.md +42 -0
- package/skills/systematic-debugging/SKILL.md +67 -0
- package/skills/ui-render-performance/SKILL.md +39 -0
- package/skills/verification-before-done/SKILL.md +57 -0
- package/skills/worktree-isolation/SKILL.md +39 -0
- package/src/agents/agent-config.ts +6 -0
- package/src/agents/agent-search.ts +98 -0
- package/src/agents/agent-serializer.ts +38 -34
- package/src/agents/discover-agents.ts +29 -15
- package/src/config/config.ts +72 -24
- package/src/config/defaults.ts +25 -0
- package/src/extension/autonomous-policy.ts +26 -33
- package/src/extension/help.ts +1 -0
- package/src/extension/management.ts +5 -0
- package/src/extension/project-init.ts +62 -2
- package/src/extension/register.ts +69 -22
- package/src/extension/registration/commands.ts +64 -25
- package/src/extension/registration/compaction-guard.ts +1 -1
- package/src/extension/registration/subagent-helpers.ts +8 -0
- package/src/extension/registration/subagent-tools.ts +149 -148
- package/src/extension/registration/team-tool.ts +14 -10
- package/src/extension/run-index.ts +35 -21
- package/src/extension/run-maintenance.ts +30 -5
- package/src/extension/team-tool/api.ts +47 -9
- package/src/extension/team-tool/cancel.ts +109 -5
- package/src/extension/team-tool/context.ts +8 -0
- package/src/extension/team-tool/intent-policy.ts +42 -0
- package/src/extension/team-tool/lifecycle-actions.ts +120 -79
- package/src/extension/team-tool/parallel-dispatch.ts +156 -0
- package/src/extension/team-tool/respond.ts +46 -18
- package/src/extension/team-tool/run.ts +55 -12
- package/src/extension/team-tool/status.ts +13 -2
- package/src/extension/team-tool-types.ts +3 -0
- package/src/extension/team-tool.ts +45 -14
- package/src/hooks/registry.ts +61 -0
- package/src/hooks/types.ts +41 -0
- package/src/observability/event-to-metric.ts +8 -1
- package/src/runtime/agent-control.ts +169 -63
- package/src/runtime/async-runner.ts +3 -1
- package/src/runtime/background-runner.ts +78 -53
- package/src/runtime/cancellation-token.ts +89 -0
- package/src/runtime/cancellation.ts +61 -0
- package/src/runtime/capability-inventory.ts +116 -0
- package/src/runtime/child-pi.ts +458 -444
- package/src/runtime/code-summary.ts +247 -0
- package/src/runtime/crash-recovery.ts +182 -0
- package/src/runtime/crew-agent-records.ts +70 -10
- package/src/runtime/crew-agent-runtime.ts +1 -0
- package/src/runtime/custom-tools/irc-tool.ts +201 -0
- package/src/runtime/custom-tools/submit-result-tool.ts +90 -0
- package/src/runtime/deadletter.ts +1 -0
- package/src/runtime/delivery-coordinator.ts +48 -25
- package/src/runtime/effectiveness.ts +81 -0
- package/src/runtime/event-stream-bridge.ts +90 -0
- package/src/runtime/live-agent-control.ts +2 -1
- package/src/runtime/live-agent-manager.ts +179 -85
- package/src/runtime/live-control-realtime.ts +1 -1
- package/src/runtime/live-extension-bridge.ts +150 -0
- package/src/runtime/live-irc.ts +92 -0
- package/src/runtime/live-session-health.ts +100 -0
- package/src/runtime/live-session-runtime.ts +599 -305
- package/src/runtime/manifest-cache.ts +17 -2
- package/src/runtime/mcp-proxy.ts +113 -0
- package/src/runtime/model-fallback.ts +6 -4
- package/src/runtime/notebook-helpers.ts +90 -0
- package/src/runtime/orphan-sentinel.ts +7 -0
- package/src/runtime/output-validator.ts +187 -0
- package/src/runtime/parallel-utils.ts +57 -0
- package/src/runtime/parent-guard.ts +80 -0
- package/src/runtime/pi-args.ts +18 -3
- package/src/runtime/process-status.ts +5 -1
- package/src/runtime/prose-compressor.ts +164 -0
- package/src/runtime/result-extractor.ts +121 -0
- package/src/runtime/retry-executor.ts +81 -64
- package/src/runtime/runtime-resolver.ts +23 -10
- package/src/runtime/semaphore.ts +131 -0
- package/src/runtime/sensitive-paths.ts +92 -0
- package/src/runtime/skill-instructions.ts +222 -0
- package/src/runtime/stale-reconciler.ts +4 -14
- package/src/runtime/stream-preview.ts +177 -0
- package/src/runtime/subagent-manager.ts +6 -2
- package/src/runtime/subprocess-tool-registry.ts +67 -0
- package/src/runtime/task-output-context.ts +177 -127
- package/src/runtime/task-runner/capabilities.ts +78 -0
- package/src/runtime/task-runner/live-executor.ts +107 -101
- package/src/runtime/task-runner/prompt-builder.ts +72 -8
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -0
- package/src/runtime/task-runner/run-projection.ts +104 -0
- package/src/runtime/task-runner.ts +115 -5
- package/src/runtime/team-runner.ts +134 -19
- package/src/runtime/workspace-tree.ts +298 -0
- package/src/runtime/yield-handler.ts +189 -0
- package/src/schema/config-schema.ts +7 -0
- package/src/schema/team-tool-schema.ts +14 -4
- package/src/skills/discover-skills.ts +67 -0
- package/src/state/active-run-registry.ts +167 -0
- package/src/state/artifact-store.ts +4 -1
- package/src/state/atomic-write.ts +50 -1
- package/src/state/blob-store.ts +117 -0
- package/src/state/contracts.ts +2 -1
- package/src/state/event-log-rotation.ts +158 -0
- package/src/state/event-log.ts +52 -2
- package/src/state/mailbox.ts +129 -9
- package/src/state/state-store.ts +32 -5
- package/src/state/types.ts +64 -2
- package/src/teams/team-config.ts +1 -0
- package/src/ui/agent-management-overlay.ts +144 -0
- package/src/ui/crew-widget.ts +15 -5
- package/src/ui/dashboard-panes/cancellation-pane.ts +43 -0
- package/src/ui/dashboard-panes/capability-pane.ts +60 -0
- package/src/ui/dashboard-panes/mailbox-pane.ts +35 -11
- package/src/ui/dashboard-panes/progress-pane.ts +2 -0
- package/src/ui/live-run-sidebar.ts +4 -0
- package/src/ui/powerbar-publisher.ts +77 -15
- package/src/ui/render-coalescer.ts +51 -0
- package/src/ui/run-dashboard.ts +4 -0
- package/src/ui/run-event-bus.ts +209 -0
- package/src/ui/run-snapshot-cache.ts +78 -18
- package/src/ui/snapshot-types.ts +10 -0
- package/src/ui/transcript-entries.ts +258 -0
- package/src/utils/ids.ts +5 -0
- package/src/utils/incremental-reader.ts +104 -0
- package/src/utils/paths.ts +4 -2
- package/src/utils/scan-cache.ts +137 -0
- package/src/utils/sse-parser.ts +134 -0
- package/src/utils/task-name-generator.ts +337 -0
- package/src/utils/visual.ts +33 -2
- package/src/workflows/workflow-config.ts +1 -0
- package/src/worktree/cleanup.ts +2 -1
|
@@ -51,6 +51,10 @@ export function hasStaleAsyncProcess(run: TeamRunManifest): boolean {
|
|
|
51
51
|
|
|
52
52
|
export function isDisplayActiveRun(run: TeamRunManifest, agents: CrewAgentRecord[] = [], now = Date.now()): boolean {
|
|
53
53
|
if (!isActiveRunStatus(run.status) || hasStaleAsyncProcess(run) || isLikelyOrphanedActiveRun(run, agents, now)) return false;
|
|
54
|
-
|
|
54
|
+
// Keep the always-visible widget quiet until a worker actually exists.
|
|
55
|
+
// Empty active manifests can be created briefly at startup, by old fixture/scaffold
|
|
56
|
+
// runs, or from cross-cwd registry history; showing them causes noisy 0/0 rows and
|
|
57
|
+
// needless spinner redraws. The full dashboard can still list historical runs.
|
|
58
|
+
if (agents.length === 0) return false;
|
|
55
59
|
return agents.some(hasDurableActiveAgentEvidence);
|
|
56
60
|
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure TypeScript prose compression module.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by caveman's compress.js — reduces tool descriptions and other
|
|
5
|
+
* prose by removing filler words, pleasantries, hedging, and articles while
|
|
6
|
+
* preserving code, URLs, paths, identifiers, and other protected segments
|
|
7
|
+
* byte-for-byte.
|
|
8
|
+
*
|
|
9
|
+
* No external dependencies. No `any` types. Regex-only pattern matching.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Protected segment patterns (order matters — earlier patterns take priority)
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
const PROTECTED_PATTERNS: readonly RegExp[] = [
|
|
17
|
+
/```[\s\S]*?```/g, // fenced code blocks
|
|
18
|
+
/`[^`\n]+`/g, // inline code
|
|
19
|
+
/\bhttps?:\/\/\S+/gi, // URLs
|
|
20
|
+
/\b[\w.-]*[\/\\][\w.\/\\\-]+/g, // paths with / or \
|
|
21
|
+
/\b[A-Z][A-Z0-9]*(?:_[A-Z][A-Z0-9]*)+\b/g, // CONSTANT_CASE
|
|
22
|
+
/\b\w+(?:\.\w+)+\(\)/g, // dotted.method() calls
|
|
23
|
+
/[A-Za-z_][A-Za-z0-9_]*\s*\([^)]*\)/g, // function calls: name(args)
|
|
24
|
+
/\b\d+\.\d+\.\d+\b/g, // version numbers x.y.z
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Compression patterns (applied to unprotected prose only)
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
const FILLERS = /\b(?:just|really|basically|actually|simply|quite|very|essentially|literally)\b/gi;
|
|
32
|
+
|
|
33
|
+
const PLEASANTRIES = /\b(?:please|kindly|thank you|thanks|sure|certainly|of course|happy to)\b[,.]?\s*/gi;
|
|
34
|
+
|
|
35
|
+
const HEDGES = /\b(?:perhaps|maybe|might|could potentially|would like to|i think|in my opinion)\b\s*/gi;
|
|
36
|
+
|
|
37
|
+
const LEADERS = /^(?:i'?\s*ll|i will|i can|you can|we will|we can|let me|let'?\s*s)\s+/gim;
|
|
38
|
+
|
|
39
|
+
const ARTICLES = /\b(?:a|an|the)\s+(?=[a-z])/gi;
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Sentinel helpers
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
const SENTINEL_OPEN = "\x00";
|
|
46
|
+
const SENTINEL_CLOSE = "\x00";
|
|
47
|
+
const SENTINEL_RE = /\x00(\d+)\x00/g;
|
|
48
|
+
|
|
49
|
+
interface SegmentStore {
|
|
50
|
+
segments: string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function extractProtected(text: string, store: SegmentStore): string {
|
|
54
|
+
let working = text;
|
|
55
|
+
for (const re of PROTECTED_PATTERNS) {
|
|
56
|
+
// Reset lastIndex for reused RegExp objects
|
|
57
|
+
re.lastIndex = 0;
|
|
58
|
+
working = working.replace(re, (match: string): string => {
|
|
59
|
+
const index = store.segments.length;
|
|
60
|
+
store.segments.push(match);
|
|
61
|
+
return `${SENTINEL_OPEN}${index}${SENTINEL_CLOSE}`;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return working;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function restoreProtected(text: string, store: SegmentStore): string {
|
|
68
|
+
return text.replace(SENTINEL_RE, (_match: string, indexStr: string): string => {
|
|
69
|
+
const index = Number(indexStr);
|
|
70
|
+
return store.segments[index] ?? "";
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Core prose compression
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
function compressProseText(text: string): string {
|
|
79
|
+
let s = text;
|
|
80
|
+
|
|
81
|
+
// Remove leading "I'll / I will / you can / ..." sentence openers
|
|
82
|
+
s = s.replace(LEADERS, "");
|
|
83
|
+
|
|
84
|
+
// Remove pleasantries
|
|
85
|
+
s = s.replace(PLEASANTRIES, "");
|
|
86
|
+
|
|
87
|
+
// Remove hedging phrases
|
|
88
|
+
s = s.replace(HEDGES, "");
|
|
89
|
+
|
|
90
|
+
// Remove filler adverbs
|
|
91
|
+
s = s.replace(FILLERS, "");
|
|
92
|
+
|
|
93
|
+
// Remove articles before lowercase words
|
|
94
|
+
s = s.replace(ARTICLES, "");
|
|
95
|
+
|
|
96
|
+
// Collapse whitespace introduced by removals
|
|
97
|
+
s = s.replace(/[ \t]{2,}/g, " ");
|
|
98
|
+
|
|
99
|
+
// Fix punctuation spacing (e.g. "word ," → "word,")
|
|
100
|
+
s = s.replace(/\s+([,.;:!?])/g, "$1");
|
|
101
|
+
|
|
102
|
+
// Collapse excessive newlines
|
|
103
|
+
s = s.replace(/\n{3,}/g, "\n\n");
|
|
104
|
+
|
|
105
|
+
// Re-capitalize first letter of each sentence after removals
|
|
106
|
+
s = s.replace(/(^|[.!?]\s+)([a-z])/g, (_match: string, pre: string, ch: string): string => {
|
|
107
|
+
return pre + ch.toUpperCase();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return s.trim();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Public API
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
export interface CompressResult {
|
|
118
|
+
compressed: string;
|
|
119
|
+
originalLength: number;
|
|
120
|
+
compressedLength: number;
|
|
121
|
+
savingsPercent: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Compress prose text by removing filler, pleasantries, hedging, and articles
|
|
126
|
+
* while preserving code blocks, URLs, paths, identifiers, and other protected
|
|
127
|
+
* segments byte-for-byte.
|
|
128
|
+
*/
|
|
129
|
+
export function compressProse(text: string): CompressResult {
|
|
130
|
+
if (typeof text !== "string" || text.length === 0) {
|
|
131
|
+
return {
|
|
132
|
+
compressed: "",
|
|
133
|
+
originalLength: 0,
|
|
134
|
+
compressedLength: 0,
|
|
135
|
+
savingsPercent: 0,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const store: SegmentStore = { segments: [] };
|
|
140
|
+
const extracted = extractProtected(text, store);
|
|
141
|
+
const compressed = compressProseText(extracted);
|
|
142
|
+
const restored = restoreProtected(compressed, store);
|
|
143
|
+
|
|
144
|
+
const originalLength = text.length;
|
|
145
|
+
const compressedLength = restored.length;
|
|
146
|
+
const savingsPercent = originalLength > 0
|
|
147
|
+
? Math.round(((originalLength - compressedLength) / originalLength) * 100 * 100) / 100
|
|
148
|
+
: 0;
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
compressed: restored,
|
|
152
|
+
originalLength,
|
|
153
|
+
compressedLength,
|
|
154
|
+
savingsPercent,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Convenience wrapper — returns just the compressed string for tool
|
|
160
|
+
* descriptions and other single-field use cases.
|
|
161
|
+
*/
|
|
162
|
+
export function compressToolDescription(description: string): string {
|
|
163
|
+
return compressProse(description).compressed;
|
|
164
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured Result Extractor — attempts to extract structured data from worker output.
|
|
3
|
+
* Tries multiple extraction strategies before falling back to raw text.
|
|
4
|
+
*/
|
|
5
|
+
export interface ExtractedResult {
|
|
6
|
+
/** Whether structured data was successfully extracted */
|
|
7
|
+
structured: boolean;
|
|
8
|
+
/** Parsed structured data (if structured=true) */
|
|
9
|
+
data: unknown;
|
|
10
|
+
/** Raw text output (always available) */
|
|
11
|
+
rawText: string;
|
|
12
|
+
/** Error message if extraction was attempted but failed */
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract structured result from raw worker output text.
|
|
18
|
+
* Tries strategies in order: direct JSON, fenced JSON, key-value markers.
|
|
19
|
+
*/
|
|
20
|
+
export function extractStructuredResult(raw: string, _schema?: Record<string, unknown>): ExtractedResult {
|
|
21
|
+
const trimmed = raw.trim();
|
|
22
|
+
if (!trimmed) {
|
|
23
|
+
return { structured: false, data: null, rawText: raw };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Strategy 1: Direct JSON parse (entire output is JSON)
|
|
27
|
+
const directResult = tryDirectJson(trimmed);
|
|
28
|
+
if (directResult !== undefined) {
|
|
29
|
+
return { structured: true, data: directResult, rawText: raw };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Strategy 2: Extract from ```json ... ``` fence
|
|
33
|
+
const fencedResult = tryFencedJson(trimmed);
|
|
34
|
+
if (fencedResult !== undefined) {
|
|
35
|
+
return { structured: true, data: fencedResult, rawText: raw };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Strategy 3: Extract from markers like "RESULT:" or "OUTPUT:"
|
|
39
|
+
const markerResult = tryMarkerExtraction(trimmed);
|
|
40
|
+
if (markerResult !== undefined) {
|
|
41
|
+
return { structured: true, data: markerResult, rawText: raw };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { structured: false, data: null, rawText: raw };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function tryDirectJson(text: string): unknown | undefined {
|
|
48
|
+
if (!text.startsWith("{") && !text.startsWith("[")) return undefined;
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(text);
|
|
51
|
+
} catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function tryFencedJson(text: string): unknown | undefined {
|
|
57
|
+
const match = text.match(/```json\s*\n([\s\S]*?)\n\s*```/);
|
|
58
|
+
if (!match?.[1]) return undefined;
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(match[1].trim());
|
|
61
|
+
} catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function tryMarkerExtraction(text: string): unknown | undefined {
|
|
67
|
+
// Try to find JSON after common markers
|
|
68
|
+
const markers = ["RESULT:", "OUTPUT:", "ANSWER:", "### Result\n", "## Output\n"];
|
|
69
|
+
for (const marker of markers) {
|
|
70
|
+
const idx = text.indexOf(marker);
|
|
71
|
+
if (idx === -1) continue;
|
|
72
|
+
const after = text.slice(idx + marker.length).trim();
|
|
73
|
+
// Try JSON parse on text after marker
|
|
74
|
+
if (after.startsWith("{") || after.startsWith("[")) {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(after);
|
|
77
|
+
} catch {
|
|
78
|
+
// Try to find just the JSON object/array
|
|
79
|
+
const jsonEnd = findMatchingBracket(after);
|
|
80
|
+
if (jsonEnd > 0) {
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(after.slice(0, jsonEnd));
|
|
83
|
+
} catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function findMatchingBracket(text: string): number {
|
|
94
|
+
const openChar = text[0];
|
|
95
|
+
const closeChar = openChar === "{" ? "}" : "]";
|
|
96
|
+
let depth = 0;
|
|
97
|
+
let inString = false;
|
|
98
|
+
let escape = false;
|
|
99
|
+
for (let i = 0; i < text.length; i++) {
|
|
100
|
+
const ch = text[i];
|
|
101
|
+
if (escape) {
|
|
102
|
+
escape = false;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (ch === "\\") {
|
|
106
|
+
escape = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (ch === '"') {
|
|
110
|
+
inString = !inString;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (inString) continue;
|
|
114
|
+
if (ch === openChar) depth++;
|
|
115
|
+
if (ch === closeChar) {
|
|
116
|
+
depth--;
|
|
117
|
+
if (depth === 0) return i + 1;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return -1;
|
|
121
|
+
}
|
|
@@ -1,64 +1,81 @@
|
|
|
1
|
-
import { sleep } from "../utils/sleep.ts";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
1
|
+
import { sleep } from "../utils/sleep.ts";
|
|
2
|
+
import { throwIfCancelled } from "./cancellation.ts";
|
|
3
|
+
|
|
4
|
+
export interface RetryPolicy {
|
|
5
|
+
maxAttempts: number;
|
|
6
|
+
backoffMs: number;
|
|
7
|
+
jitterRatio: number;
|
|
8
|
+
exponentialFactor: number;
|
|
9
|
+
retryableErrors?: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RetryAttemptInfo {
|
|
13
|
+
attempt: number;
|
|
14
|
+
attemptId: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RetryHooks {
|
|
18
|
+
onAttemptFailed?: (attempt: number, error: Error, nextDelayMs: number, info: RetryAttemptInfo) => void;
|
|
19
|
+
onRetryGivenUp?: (attempts: number, error: Error, info: RetryAttemptInfo) => void;
|
|
20
|
+
attemptId?: (attempt: number) => string;
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_RETRY_POLICY: RetryPolicy = { maxAttempts: 3, backoffMs: 1000, jitterRatio: 0.3, exponentialFactor: 2 };
|
|
25
|
+
|
|
26
|
+
function asError(error: unknown): Error {
|
|
27
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function globToRegex(pattern: string): RegExp {
|
|
31
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
32
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isRetryable(error: Error, policy: RetryPolicy): boolean {
|
|
36
|
+
const patterns = policy.retryableErrors ?? [];
|
|
37
|
+
if (!patterns.length) return true;
|
|
38
|
+
return patterns.some((pattern) => globToRegex(pattern).test(error.message));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function calculateRetryDelay(attempt: number, policy: RetryPolicy = DEFAULT_RETRY_POLICY, random = Math.random): number {
|
|
42
|
+
const base = policy.backoffMs * Math.pow(policy.exponentialFactor, Math.max(0, attempt - 1));
|
|
43
|
+
const jitter = (random() * 2 - 1) * policy.jitterRatio * base;
|
|
44
|
+
return Math.max(0, base + jitter);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function retryAttemptInfo(attempt: number, hooks: RetryHooks): RetryAttemptInfo {
|
|
48
|
+
return { attempt, attemptId: hooks.attemptId?.(attempt) ?? `retry_attempt_${attempt}` };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function executeWithRetry<T>(fn: (attempt: number, info: RetryAttemptInfo) => Promise<T>, policy: RetryPolicy = DEFAULT_RETRY_POLICY, hooks: RetryHooks = {}): Promise<T> {
|
|
52
|
+
const normalized: RetryPolicy = { ...DEFAULT_RETRY_POLICY, ...policy, maxAttempts: Math.max(1, policy.maxAttempts ?? DEFAULT_RETRY_POLICY.maxAttempts) };
|
|
53
|
+
let lastError: Error | undefined;
|
|
54
|
+
for (let attempt = 1; attempt <= normalized.maxAttempts; attempt += 1) {
|
|
55
|
+
throwIfCancelled(hooks.signal);
|
|
56
|
+
const info = retryAttemptInfo(attempt, hooks);
|
|
57
|
+
try {
|
|
58
|
+
return await fn(attempt, info);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
lastError = asError(error);
|
|
61
|
+
// Never retry if aborted — sleep() would immediately reject on every attempt.
|
|
62
|
+
if (hooks.signal?.aborted) {
|
|
63
|
+
hooks.onRetryGivenUp?.(attempt, lastError, info);
|
|
64
|
+
throw lastError;
|
|
65
|
+
}
|
|
66
|
+
if (attempt >= normalized.maxAttempts || !isRetryable(lastError, normalized)) {
|
|
67
|
+
hooks.onRetryGivenUp?.(attempt, lastError, info);
|
|
68
|
+
throw lastError;
|
|
69
|
+
}
|
|
70
|
+
const delay = calculateRetryDelay(attempt, normalized);
|
|
71
|
+
hooks.onAttemptFailed?.(attempt, lastError, delay, info);
|
|
72
|
+
try {
|
|
73
|
+
await sleep(delay, hooks.signal);
|
|
74
|
+
} catch (sleepError) {
|
|
75
|
+
if (hooks.signal?.aborted) throwIfCancelled(hooks.signal);
|
|
76
|
+
throw sleepError;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
throw lastError ?? new Error("Retry failed without error.");
|
|
81
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { PiTeamsConfig } from "../config/config.ts";
|
|
2
|
+
import type { RuntimeResolutionState } from "../state/types.ts";
|
|
2
3
|
import type { CrewRuntimeKind } from "./crew-agent-runtime.ts";
|
|
3
4
|
|
|
4
5
|
export type CrewRuntimeMode = "auto" | "scaffold" | "child-process" | "live-session";
|
|
5
6
|
|
|
7
|
+
export type CrewRuntimeSafety = "trusted" | "explicit_dry_run" | "blocked";
|
|
8
|
+
|
|
6
9
|
export interface CrewRuntimeCapabilities {
|
|
7
10
|
kind: CrewRuntimeKind;
|
|
8
11
|
requestedMode: CrewRuntimeMode;
|
|
@@ -13,12 +16,22 @@ export interface CrewRuntimeCapabilities {
|
|
|
13
16
|
liveToolActivity: boolean;
|
|
14
17
|
transcript: boolean;
|
|
15
18
|
reason?: string;
|
|
19
|
+
safety: CrewRuntimeSafety;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function runtimeResolutionState(runtime: CrewRuntimeCapabilities, resolvedAt = new Date().toISOString()): RuntimeResolutionState {
|
|
23
|
+
return {
|
|
24
|
+
kind: runtime.kind,
|
|
25
|
+
requestedMode: runtime.requestedMode,
|
|
26
|
+
safety: runtime.safety,
|
|
27
|
+
available: runtime.available,
|
|
28
|
+
...(runtime.fallback ? { fallback: runtime.fallback } : {}),
|
|
29
|
+
...(runtime.reason ? { reason: runtime.reason } : {}),
|
|
30
|
+
resolvedAt,
|
|
31
|
+
};
|
|
16
32
|
}
|
|
17
33
|
|
|
18
34
|
export async function isLiveSessionRuntimeAvailable(timeoutMs = 1500, env: NodeJS.ProcessEnv = process.env): Promise<{ available: boolean; reason?: string }> {
|
|
19
|
-
if (env.PI_CREW_ENABLE_EXPERIMENTAL_LIVE_SESSION !== "1") {
|
|
20
|
-
return { available: false, reason: "Live-session runtime adapter is experimental and disabled. Set PI_CREW_ENABLE_EXPERIMENTAL_LIVE_SESSION=1 to probe SDK support." };
|
|
21
|
-
}
|
|
22
35
|
if (env.PI_CREW_MOCK_LIVE_SESSION === "success") {
|
|
23
36
|
return { available: true, reason: "Mock live-session runtime is enabled." };
|
|
24
37
|
}
|
|
@@ -52,26 +65,26 @@ export async function isLiveSessionRuntimeAvailable(timeoutMs = 1500, env: NodeJ
|
|
|
52
65
|
export async function resolveCrewRuntime(config: PiTeamsConfig, env: NodeJS.ProcessEnv = process.env): Promise<CrewRuntimeCapabilities> {
|
|
53
66
|
const requestedMode = config.runtime?.mode ?? "auto";
|
|
54
67
|
const workersDisabled = config.executeWorkers === false || env.PI_CREW_EXECUTE_WORKERS === "0" || env.PI_TEAMS_EXECUTE_WORKERS === "0";
|
|
55
|
-
if (requestedMode === "scaffold") return scaffoldCaps(requestedMode);
|
|
56
|
-
if (workersDisabled) return scaffoldCaps(requestedMode, "Child worker execution disabled by config/env. Set runtime.mode=scaffold or executeWorkers=false only for dry runs.");
|
|
68
|
+
if (requestedMode === "scaffold") return scaffoldCaps(requestedMode, undefined, "explicit_dry_run");
|
|
69
|
+
if (workersDisabled) return scaffoldCaps(requestedMode, "Child worker execution disabled by config/env. Set runtime.mode=scaffold or executeWorkers=false only for dry runs.", "blocked");
|
|
57
70
|
if (requestedMode === "child-process") return childCaps(requestedMode);
|
|
58
71
|
if (requestedMode === "live-session" || (requestedMode === "auto" && config.runtime?.preferLiveSession === true)) {
|
|
59
72
|
const live = await isLiveSessionRuntimeAvailable(1500, env);
|
|
60
73
|
if (live.available) return liveCaps(requestedMode);
|
|
61
|
-
if (requestedMode === "live-session" && config.runtime?.allowChildProcessFallback === false) return
|
|
74
|
+
if (requestedMode === "live-session" && config.runtime?.allowChildProcessFallback === false) return scaffoldCaps(requestedMode, live.reason, "blocked");
|
|
62
75
|
return { ...childCaps(requestedMode), fallback: "child-process", reason: live.reason };
|
|
63
76
|
}
|
|
64
77
|
return childCaps(requestedMode);
|
|
65
78
|
}
|
|
66
79
|
|
|
67
|
-
function scaffoldCaps(requestedMode: CrewRuntimeMode, reason?: string): CrewRuntimeCapabilities {
|
|
68
|
-
return { kind: "scaffold", requestedMode, available:
|
|
80
|
+
function scaffoldCaps(requestedMode: CrewRuntimeMode, reason?: string, safety: CrewRuntimeSafety = "explicit_dry_run"): CrewRuntimeCapabilities {
|
|
81
|
+
return { kind: "scaffold", requestedMode, available: safety !== "blocked", steer: false, resume: false, liveToolActivity: false, transcript: false, safety, ...(reason ? { reason } : {}) };
|
|
69
82
|
}
|
|
70
83
|
|
|
71
84
|
function childCaps(requestedMode: CrewRuntimeMode, reason?: string): CrewRuntimeCapabilities {
|
|
72
|
-
return { kind: "child-process", requestedMode, available: true, steer: false, resume: false, liveToolActivity: false, transcript: true, ...(reason ? { reason } : {}) };
|
|
85
|
+
return { kind: "child-process", requestedMode, available: true, steer: false, resume: false, liveToolActivity: false, transcript: true, safety: "trusted", ...(reason ? { reason } : {}) };
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
function liveCaps(requestedMode: CrewRuntimeMode): CrewRuntimeCapabilities {
|
|
76
|
-
return { kind: "live-session", requestedMode, available: true, steer: true, resume: true, liveToolActivity: true, transcript: true };
|
|
89
|
+
return { kind: "live-session", requestedMode, available: true, steer: true, resume: true, liveToolActivity: true, transcript: true, safety: "trusted" };
|
|
77
90
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 6: Semaphore and fail-fast parallel execution.
|
|
3
|
+
*
|
|
4
|
+
* Adapted from oh-my-pi's `parallel.ts` Semaphore class and
|
|
5
|
+
* `mapWithConcurrencyLimit` implementation. Provides:
|
|
6
|
+
* - Explicit acquire/release Semaphore for concurrency control
|
|
7
|
+
* - Fail-fast on first error (via Promise.race)
|
|
8
|
+
* - AbortSignal support for graceful cancellation
|
|
9
|
+
* - Partial results on abort
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Simple counting semaphore for limiting concurrency across independently-scheduled async work.
|
|
14
|
+
*/
|
|
15
|
+
export class Semaphore {
|
|
16
|
+
#max: number;
|
|
17
|
+
#current = 0;
|
|
18
|
+
#queue: Array<() => void> = [];
|
|
19
|
+
|
|
20
|
+
constructor(max: number) {
|
|
21
|
+
this.#max = Math.max(1, max);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async acquire(): Promise<void> {
|
|
25
|
+
if (this.#current < this.#max) {
|
|
26
|
+
this.#current++;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const { promise, resolve } = (() => {
|
|
30
|
+
let res: () => void;
|
|
31
|
+
const p = new Promise<void>((r) => { res = r; });
|
|
32
|
+
return { promise: p, resolve: res! };
|
|
33
|
+
})();
|
|
34
|
+
this.#queue.push(resolve);
|
|
35
|
+
return promise;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
release(): void {
|
|
39
|
+
const next = this.#queue.shift();
|
|
40
|
+
if (next) {
|
|
41
|
+
next();
|
|
42
|
+
} else if (this.#current > 0) {
|
|
43
|
+
this.#current--;
|
|
44
|
+
}
|
|
45
|
+
// Guard: over-release is a no-op to prevent #current going negative
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Current number of acquired slots. */
|
|
49
|
+
get current(): number {
|
|
50
|
+
return this.#current;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Number of waiters in the queue. */
|
|
54
|
+
get waiting(): number {
|
|
55
|
+
return this.#queue.length;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Result of parallel execution with fail-fast support.
|
|
61
|
+
*/
|
|
62
|
+
export interface ParallelResult<R> {
|
|
63
|
+
/** Results array — undefined entries indicate tasks that were skipped due to abort. */
|
|
64
|
+
results: (R | undefined)[];
|
|
65
|
+
/** Whether execution was aborted before all tasks completed. */
|
|
66
|
+
aborted: boolean;
|
|
67
|
+
/** The first error that triggered fail-fast, if any. */
|
|
68
|
+
firstError?: unknown;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Execute items with a concurrency limit, fail-fast, and abort signal support.
|
|
73
|
+
*
|
|
74
|
+
* - On first error: aborts remaining workers and rethrows.
|
|
75
|
+
* - On external abort: returns partial results with `aborted: true`.
|
|
76
|
+
* - Results are returned in the same order as input items.
|
|
77
|
+
*
|
|
78
|
+
* Adapted from oh-my-pi's `mapWithConcurrencyLimit`.
|
|
79
|
+
*/
|
|
80
|
+
export async function mapWithFailFast<T, R>(
|
|
81
|
+
items: T[],
|
|
82
|
+
concurrency: number,
|
|
83
|
+
fn: (item: T, index: number, signal: AbortSignal) => Promise<R>,
|
|
84
|
+
signal?: AbortSignal,
|
|
85
|
+
): Promise<ParallelResult<R>> {
|
|
86
|
+
const limit = Math.max(1, Math.min(concurrency, items.length));
|
|
87
|
+
const results: (R | undefined)[] = new Array(items.length);
|
|
88
|
+
let nextIndex = 0;
|
|
89
|
+
|
|
90
|
+
// Internal abort controller for fail-fast
|
|
91
|
+
const abortController = new AbortController();
|
|
92
|
+
const workerSignal = signal
|
|
93
|
+
? AbortSignal.any([signal, abortController.signal])
|
|
94
|
+
: abortController.signal;
|
|
95
|
+
|
|
96
|
+
// Promise that rejects on first error — used for fail-fast
|
|
97
|
+
let rejectFirst: (error: unknown) => void;
|
|
98
|
+
const firstErrorPromise = new Promise<never>((_, reject) => {
|
|
99
|
+
rejectFirst = reject;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const worker = async (): Promise<void> => {
|
|
103
|
+
while (true) {
|
|
104
|
+
if (workerSignal.aborted) return;
|
|
105
|
+
const index = nextIndex++;
|
|
106
|
+
if (index >= items.length) return;
|
|
107
|
+
try {
|
|
108
|
+
results[index] = await fn(items[index], index, workerSignal);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (!workerSignal.aborted) {
|
|
111
|
+
abortController.abort();
|
|
112
|
+
rejectFirst(error);
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const workers = Array.from({ length: limit }, () => worker());
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
await Promise.race([Promise.all(workers), firstErrorPromise]);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (signal?.aborted) {
|
|
125
|
+
return { results, aborted: true, firstError: error };
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { results, aborted: signal?.aborted ?? false };
|
|
131
|
+
}
|