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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { HookDefinition, HookName, HookContext, HookResult, HookExecutionReport } from "./types.ts";
|
|
2
|
+
import { appendEvent } from "../state/event-log.ts";
|
|
3
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
4
|
+
import { runEventBus } from "../ui/run-event-bus.ts";
|
|
5
|
+
|
|
6
|
+
const registry = new Map<HookName, HookDefinition[]>();
|
|
7
|
+
|
|
8
|
+
export function registerHook(definition: HookDefinition): void {
|
|
9
|
+
const hooks = registry.get(definition.name) ?? [];
|
|
10
|
+
hooks.push(definition);
|
|
11
|
+
registry.set(definition.name, hooks);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function clearHooks(): void {
|
|
15
|
+
registry.clear();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getHooks(name: HookName): HookDefinition[] {
|
|
19
|
+
return registry.get(name) ?? [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function executeHook(name: HookName, ctx: HookContext): Promise<HookExecutionReport> {
|
|
23
|
+
const hooks = getHooks(name);
|
|
24
|
+
if (hooks.length === 0) return { hookName: name, outcome: "allow", durationMs: 0 };
|
|
25
|
+
const start = Date.now();
|
|
26
|
+
const diagnostics: string[] = [];
|
|
27
|
+
let capturedModifications: Record<string, unknown> | undefined;
|
|
28
|
+
for (const hook of hooks) {
|
|
29
|
+
try {
|
|
30
|
+
const result: HookResult = await hook.handler(ctx);
|
|
31
|
+
if (hook.mode === "blocking" && result.outcome === "block") {
|
|
32
|
+
return { hookName: name, outcome: "block", durationMs: Date.now() - start, reason: result.reason };
|
|
33
|
+
}
|
|
34
|
+
if (result.outcome === "modify" && result.data) {
|
|
35
|
+
Object.assign(ctx, result.data);
|
|
36
|
+
capturedModifications = { ...result.data };
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
40
|
+
if (hook.mode === "blocking") {
|
|
41
|
+
return { hookName: name, outcome: "block", durationMs: Date.now() - start, reason: `Hook error: ${message}` };
|
|
42
|
+
}
|
|
43
|
+
// Non-blocking hook errors are accumulated as diagnostics; continue to next hook
|
|
44
|
+
diagnostics.push(message);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (diagnostics.length > 0) {
|
|
48
|
+
return { hookName: name, outcome: "diagnostic", durationMs: Date.now() - start, reason: diagnostics.join("; "), modifiedData: capturedModifications };
|
|
49
|
+
}
|
|
50
|
+
return { hookName: name, outcome: "allow", durationMs: Date.now() - start, modifiedData: capturedModifications };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function appendHookEvent(manifest: TeamRunManifest, report: HookExecutionReport): void {
|
|
54
|
+
appendEvent(manifest.eventsPath, {
|
|
55
|
+
type: "hook.executed",
|
|
56
|
+
runId: manifest.runId,
|
|
57
|
+
message: `Hook ${report.hookName} completed with outcome=${report.outcome}${report.reason ? `: ${report.reason}` : ""}`,
|
|
58
|
+
data: { hookName: report.hookName, outcome: report.outcome, durationMs: report.durationMs, reason: report.reason },
|
|
59
|
+
});
|
|
60
|
+
runEventBus.emit({ type: "effectiveness_changed", runId: manifest.runId, data: { hookName: report.hookName, outcome: report.outcome } });
|
|
61
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type HookName =
|
|
2
|
+
| "before_run_start"
|
|
3
|
+
| "before_task_start"
|
|
4
|
+
| "task_result"
|
|
5
|
+
| "before_cancel"
|
|
6
|
+
| "before_retry"
|
|
7
|
+
| "before_forget"
|
|
8
|
+
| "before_cleanup"
|
|
9
|
+
| "before_publish"
|
|
10
|
+
| "session_before_switch"
|
|
11
|
+
| "run_recovery";
|
|
12
|
+
|
|
13
|
+
export type HookMode = "blocking" | "non_blocking";
|
|
14
|
+
export type HookOutcome = "allow" | "block" | "modify" | "diagnostic";
|
|
15
|
+
|
|
16
|
+
export interface HookContext {
|
|
17
|
+
runId: string;
|
|
18
|
+
taskId?: string;
|
|
19
|
+
cwd: string;
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface HookResult {
|
|
24
|
+
outcome: HookOutcome;
|
|
25
|
+
reason?: string;
|
|
26
|
+
data?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface HookDefinition {
|
|
30
|
+
name: HookName;
|
|
31
|
+
mode: HookMode;
|
|
32
|
+
handler: (ctx: HookContext) => HookResult | Promise<HookResult>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface HookExecutionReport {
|
|
36
|
+
hookName: HookName;
|
|
37
|
+
outcome: HookOutcome;
|
|
38
|
+
durationMs: number;
|
|
39
|
+
reason?: string;
|
|
40
|
+
modifiedData?: Record<string, unknown>;
|
|
41
|
+
}
|
|
@@ -13,6 +13,13 @@ function numberValue(value: unknown, fallback = 0): number {
|
|
|
13
13
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
const CANCELLATION_REASON_LABELS = new Set(["caller_cancelled", "leader_interrupted", "provider_timeout", "worker_timeout", "tool_timeout", "shutdown", "unknown"]);
|
|
17
|
+
|
|
18
|
+
function cancellationReasonLabel(value: unknown): string {
|
|
19
|
+
const raw = stringValue(value, "unknown");
|
|
20
|
+
return CANCELLATION_REASON_LABELS.has(raw) ? raw : "unknown";
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
export interface EventToMetricSubscription {
|
|
17
24
|
dispose(): void;
|
|
18
25
|
}
|
|
@@ -36,7 +43,7 @@ export function wireEventToMetrics(events: ExtensionAPI["events"] | undefined, r
|
|
|
36
43
|
const handlers: Array<[string, (data: unknown) => void]> = [
|
|
37
44
|
["crew.run.completed", (data) => { const item = recordValue(data); runCount.inc({ status: "completed" }); runDuration.observe({ team: stringValue(item.team, "unknown") }, numberValue(item.durationMs)); }],
|
|
38
45
|
["crew.run.failed", () => runCount.inc({ status: "failed" })],
|
|
39
|
-
["crew.run.cancelled", () => runCount.inc({ status: "cancelled" })],
|
|
46
|
+
["crew.run.cancelled", (data) => { const item = recordValue(data); runCount.inc({ status: "cancelled", reason: cancellationReasonLabel(item.reason) }); }],
|
|
40
47
|
["crew.task.completed", (data) => { const item = recordValue(data); taskCount.inc({ status: "completed" }); taskDuration.observe({ role: stringValue(item.role, "unknown") }, numberValue(item.durationMs)); tokenUsage.observe({ role: stringValue(item.role, "unknown") }, numberValue(item.tokens)); }],
|
|
41
48
|
["crew.task.failed", () => taskCount.inc({ status: "failed" })],
|
|
42
49
|
["crew.task.retry_attempt", (data) => { const item = recordValue(data); taskCount.inc({ status: "retry" }); retryAttemptCount.inc({ runId: stringValue(item.runId, "unknown"), taskId: stringValue(item.taskId, "unknown") }); }],
|
|
@@ -1,63 +1,169 @@
|
|
|
1
|
-
import type { PiTeamsConfig } from "../config/config.ts";
|
|
2
|
-
import type { TeamRunManifest } from "../state/types.ts";
|
|
3
|
-
import { appendTaskAttentionEvent } from "./attention-events.ts";
|
|
4
|
-
import type { CrewAgentRecord } from "./crew-agent-runtime.ts";
|
|
5
|
-
import { upsertCrewAgent } from "./crew-agent-records.ts";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function
|
|
34
|
-
const
|
|
35
|
-
if (
|
|
36
|
-
|
|
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
|
-
|
|
1
|
+
import type { PiTeamsConfig } from "../config/config.ts";
|
|
2
|
+
import type { TeamRunManifest, ControlReservation } from "../state/types.ts";
|
|
3
|
+
import { appendTaskAttentionEvent } from "./attention-events.ts";
|
|
4
|
+
import type { CrewAgentRecord } from "./crew-agent-runtime.ts";
|
|
5
|
+
import { upsertCrewAgent } from "./crew-agent-records.ts";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
|
|
8
|
+
export interface CrewControlConfig {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
needsAttentionAfterMs: number;
|
|
11
|
+
consecutiveFailureThreshold: number;
|
|
12
|
+
longRunningMinutes: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DEFAULT_NEEDS_ATTENTION_MS = 60_000;
|
|
16
|
+
const DEFAULT_CONSECUTIVE_FAILURE_THRESHOLD = 3;
|
|
17
|
+
const DEFAULT_LONG_RUNNING_MINUTES = 10;
|
|
18
|
+
|
|
19
|
+
function positiveInt(value: unknown): number | undefined {
|
|
20
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resolveCrewControlConfig(config: PiTeamsConfig | undefined): CrewControlConfig {
|
|
24
|
+
const raw = config as PiTeamsConfig & { control?: { enabled?: unknown; needsAttentionAfterMs?: unknown; consecutiveFailureThreshold?: unknown; longRunningMinutes?: unknown } } | undefined;
|
|
25
|
+
return {
|
|
26
|
+
enabled: raw?.control?.enabled === false ? false : true,
|
|
27
|
+
needsAttentionAfterMs: positiveInt(raw?.control?.needsAttentionAfterMs) ?? DEFAULT_NEEDS_ATTENTION_MS,
|
|
28
|
+
consecutiveFailureThreshold: positiveInt(raw?.control?.consecutiveFailureThreshold) ?? DEFAULT_CONSECUTIVE_FAILURE_THRESHOLD,
|
|
29
|
+
longRunningMinutes: positiveInt(raw?.control?.longRunningMinutes) ?? DEFAULT_LONG_RUNNING_MINUTES,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function activityAgeMs(agent: CrewAgentRecord, now = Date.now()): number | undefined {
|
|
34
|
+
const timestamp = agent.progress?.lastActivityAt ?? agent.startedAt;
|
|
35
|
+
if (!timestamp) return undefined;
|
|
36
|
+
const ms = now - new Date(timestamp).getTime();
|
|
37
|
+
return Number.isFinite(ms) ? Math.max(0, ms) : undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function formatActivityAge(agent: CrewAgentRecord, now = Date.now()): string | undefined {
|
|
41
|
+
const age = activityAgeMs(agent, now);
|
|
42
|
+
if (age === undefined) return undefined;
|
|
43
|
+
if (age < 1000) return "active now";
|
|
44
|
+
const seconds = Math.floor(age / 1000);
|
|
45
|
+
if (seconds < 60) return agent.progress?.activityState === "needs_attention" ? `no activity for ${seconds}s` : `active ${seconds}s ago`;
|
|
46
|
+
const minutes = Math.floor(seconds / 60);
|
|
47
|
+
return agent.progress?.activityState === "needs_attention" ? `no activity for ${minutes}m` : `active ${minutes}m ago`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function applyAttentionState(manifest: TeamRunManifest, agent: CrewAgentRecord, config: CrewControlConfig, now = Date.now()): CrewAgentRecord {
|
|
51
|
+
if (!config.enabled || agent.status !== "running") return agent;
|
|
52
|
+
const age = activityAgeMs(agent, now);
|
|
53
|
+
if (age === undefined || age <= config.needsAttentionAfterMs) return agent;
|
|
54
|
+
if (agent.progress?.activityState === "needs_attention") return agent;
|
|
55
|
+
const updated: CrewAgentRecord = {
|
|
56
|
+
...agent,
|
|
57
|
+
progress: {
|
|
58
|
+
...(agent.progress ?? { recentTools: [], recentOutput: [], toolCount: agent.toolUses ?? 0 }),
|
|
59
|
+
activityState: "needs_attention",
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
upsertCrewAgent(manifest, updated);
|
|
63
|
+
appendTaskAttentionEvent({
|
|
64
|
+
manifest,
|
|
65
|
+
taskId: agent.taskId,
|
|
66
|
+
message: `${agent.agent} needs attention (no observed activity for ${Math.floor(age / 1000)}s).`,
|
|
67
|
+
data: { activityState: "needs_attention", reason: "idle", elapsedMs: age, taskId: agent.taskId, agentName: agent.agent, suggestedAction: "Check worker status, wait, steer, or cancel if needed." },
|
|
68
|
+
});
|
|
69
|
+
return updated;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function applyLongRunningCheck(
|
|
73
|
+
manifest: TeamRunManifest,
|
|
74
|
+
agent: CrewAgentRecord,
|
|
75
|
+
config: CrewControlConfig,
|
|
76
|
+
now = Date.now(),
|
|
77
|
+
): CrewAgentRecord {
|
|
78
|
+
if (!config.enabled || agent.status !== "running") return agent;
|
|
79
|
+
if (agent.progress?.activityState === "needs_attention") return agent;
|
|
80
|
+
|
|
81
|
+
const startedAt = agent.startedAt ? new Date(agent.startedAt).getTime() : undefined;
|
|
82
|
+
if (!startedAt) return agent;
|
|
83
|
+
|
|
84
|
+
const runtimeMs = now - startedAt;
|
|
85
|
+
const thresholdMs = config.longRunningMinutes * 60 * 1000;
|
|
86
|
+
if (runtimeMs <= thresholdMs) return agent;
|
|
87
|
+
|
|
88
|
+
// Already flagged as long_running
|
|
89
|
+
if (agent.progress?.activityState === "active_long_running") return agent;
|
|
90
|
+
|
|
91
|
+
const updated: CrewAgentRecord = {
|
|
92
|
+
...agent,
|
|
93
|
+
progress: {
|
|
94
|
+
...(agent.progress ?? { recentTools: [], recentOutput: [], toolCount: agent.toolUses ?? 0 }),
|
|
95
|
+
activityState: "active_long_running",
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
upsertCrewAgent(manifest, updated);
|
|
99
|
+
appendTaskAttentionEvent({
|
|
100
|
+
manifest,
|
|
101
|
+
taskId: agent.taskId,
|
|
102
|
+
message: `${agent.agent} has been running for ${Math.floor(runtimeMs / 60000)}m (threshold: ${config.longRunningMinutes}m).`,
|
|
103
|
+
data: { activityState: "active_long_running", reason: "idle", elapsedMs: runtimeMs, taskId: agent.taskId, agentName: agent.agent, suggestedAction: "Check worker progress, steer, or cancel if needed." },
|
|
104
|
+
});
|
|
105
|
+
return updated;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function trackConsecutiveToolFailure(
|
|
109
|
+
manifest: TeamRunManifest,
|
|
110
|
+
agent: CrewAgentRecord,
|
|
111
|
+
toolName: string,
|
|
112
|
+
error: string | undefined,
|
|
113
|
+
config: CrewControlConfig,
|
|
114
|
+
): CrewAgentRecord {
|
|
115
|
+
if (!config.enabled || agent.status !== "running") return agent;
|
|
116
|
+
|
|
117
|
+
const failures = agent.progress?.consecutiveFailures ?? 0;
|
|
118
|
+
const newFailures = failures + 1;
|
|
119
|
+
|
|
120
|
+
const updated: CrewAgentRecord = {
|
|
121
|
+
...agent,
|
|
122
|
+
progress: {
|
|
123
|
+
...(agent.progress ?? { recentTools: [], recentOutput: [], toolCount: agent.toolUses ?? 0 }),
|
|
124
|
+
consecutiveFailures: newFailures,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
if (newFailures >= config.consecutiveFailureThreshold) {
|
|
129
|
+
upsertCrewAgent(manifest, updated);
|
|
130
|
+
appendTaskAttentionEvent({
|
|
131
|
+
manifest,
|
|
132
|
+
taskId: agent.taskId,
|
|
133
|
+
message: `${agent.agent} has ${newFailures} consecutive tool failures (threshold: ${config.consecutiveFailureThreshold}). Last: ${toolName}${error ? ` - ${error.slice(0, 100)}` : ""}`,
|
|
134
|
+
data: { activityState: "needs_attention", reason: "tool_failures", taskId: agent.taskId, agentName: agent.agent, suggestedAction: "Investigate tool failures, steer, or cancel if needed." },
|
|
135
|
+
});
|
|
136
|
+
} else {
|
|
137
|
+
upsertCrewAgent(manifest, updated);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return updated;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function resetConsecutiveToolFailures(
|
|
144
|
+
manifest: TeamRunManifest,
|
|
145
|
+
agent: CrewAgentRecord,
|
|
146
|
+
): void {
|
|
147
|
+
if (!agent.progress?.consecutiveFailures) return;
|
|
148
|
+
const updated: CrewAgentRecord = {
|
|
149
|
+
...agent,
|
|
150
|
+
progress: {
|
|
151
|
+
...agent.progress,
|
|
152
|
+
consecutiveFailures: 0,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
upsertCrewAgent(manifest, updated);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Reserve a control channel for a task before spawning its worker.
|
|
160
|
+
* This ensures cancel/steer requests can be queued immediately
|
|
161
|
+
* while the worker is still starting up.
|
|
162
|
+
*/
|
|
163
|
+
export function reserveControlChannel(taskId: string, runId: string): ControlReservation {
|
|
164
|
+
return {
|
|
165
|
+
reservedAt: new Date().toISOString(),
|
|
166
|
+
controllerId: `ctrl:${taskId}:${randomUUID()}`,
|
|
167
|
+
acceptsControlEvents: true,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -6,6 +6,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
6
6
|
import { appendEvent } from "../state/event-log.ts";
|
|
7
7
|
import type { TeamRunManifest } from "../state/types.ts";
|
|
8
8
|
|
|
9
|
+
|
|
9
10
|
export type FileExists = (filePath: string) => boolean;
|
|
10
11
|
|
|
11
12
|
const requireFromHere = createRequire(import.meta.url);
|
|
@@ -49,7 +50,7 @@ export function buildBackgroundSpawnOptions(manifest: TeamRunManifest, logFd: nu
|
|
|
49
50
|
cwd: manifest.cwd,
|
|
50
51
|
detached: true,
|
|
51
52
|
stdio: ["ignore", logFd, logFd],
|
|
52
|
-
env: { ...process.env },
|
|
53
|
+
env: { ...process.env, PI_CREW_PARENT_PID: String(process.pid) },
|
|
53
54
|
windowsHide: true,
|
|
54
55
|
};
|
|
55
56
|
}
|
|
@@ -70,6 +71,7 @@ export function spawnBackgroundTeamRun(manifest: TeamRunManifest): SpawnBackgrou
|
|
|
70
71
|
fs.appendFileSync(logPath, `[pi-crew] background loader=${command.loader}\n`, "utf-8");
|
|
71
72
|
const child = spawn(process.execPath, command.args, buildBackgroundSpawnOptions(manifest, logFd));
|
|
72
73
|
child.unref();
|
|
74
|
+
|
|
73
75
|
return { pid: child.pid, logPath };
|
|
74
76
|
} finally {
|
|
75
77
|
fs.closeSync(logFd);
|
|
@@ -1,53 +1,78 @@
|
|
|
1
|
-
import { allAgents, discoverAgents } from "../agents/discover-agents.ts";
|
|
2
|
-
import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
|
|
3
|
-
import { appendEvent } from "../state/event-log.ts";
|
|
4
|
-
import { loadRunManifestById, updateRunStatus } from "../state/state-store.ts";
|
|
5
|
-
import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
|
|
6
|
-
import { loadConfig } from "../config/config.ts";
|
|
7
|
-
import { executeTeamRun } from "./team-runner.ts";
|
|
8
|
-
import { resolveCrewRuntime } from "./runtime-resolver.ts";
|
|
9
|
-
import { directTeamAndWorkflowFromRun } from "./direct-run.ts";
|
|
10
|
-
import { expandParallelResearchWorkflow } from "./parallel-research.ts";
|
|
11
|
-
import { writeAsyncStartMarker } from "./async-marker.ts";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
1
|
+
import { allAgents, discoverAgents } from "../agents/discover-agents.ts";
|
|
2
|
+
import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
|
|
3
|
+
import { appendEvent } from "../state/event-log.ts";
|
|
4
|
+
import { loadRunManifestById, saveRunManifest, updateRunStatus } from "../state/state-store.ts";
|
|
5
|
+
import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
|
|
6
|
+
import { loadConfig } from "../config/config.ts";
|
|
7
|
+
import { executeTeamRun } from "./team-runner.ts";
|
|
8
|
+
import { resolveCrewRuntime, runtimeResolutionState } from "./runtime-resolver.ts";
|
|
9
|
+
import { directTeamAndWorkflowFromRun } from "./direct-run.ts";
|
|
10
|
+
import { expandParallelResearchWorkflow } from "./parallel-research.ts";
|
|
11
|
+
import { writeAsyncStartMarker } from "./async-marker.ts";
|
|
12
|
+
import { startParentGuard, stopParentGuard } from "./parent-guard.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Remove macOS malloc-stack-logging vars that get inherited by child shells.
|
|
16
|
+
* Without this, every subprocess prints "MallocStackLogging: can't turn off..." to stderr.
|
|
17
|
+
*/
|
|
18
|
+
function scrubProcessEnv(): void {
|
|
19
|
+
delete process.env.MallocStackLogging;
|
|
20
|
+
delete process.env.MallocStackLoggingNoCompact;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function argValue(name: string): string | undefined {
|
|
24
|
+
const index = process.argv.indexOf(name);
|
|
25
|
+
if (index === -1) return undefined;
|
|
26
|
+
return process.argv[index + 1];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function main(): Promise<void> {
|
|
30
|
+
// Scrub macOS malloc vars BEFORE anything else — must be clean for all child processes
|
|
31
|
+
scrubProcessEnv();
|
|
32
|
+
|
|
33
|
+
// Start parent guard FIRST — if parent is already dead, exit immediately
|
|
34
|
+
const parentPid = Number(process.env.PI_CREW_PARENT_PID);
|
|
35
|
+
if (parentPid > 0) startParentGuard(parentPid);
|
|
36
|
+
|
|
37
|
+
const cwd = argValue("--cwd");
|
|
38
|
+
const runId = argValue("--run-id");
|
|
39
|
+
if (!cwd || !runId) throw new Error("Usage: background-runner.ts --cwd <cwd> --run-id <runId>");
|
|
40
|
+
|
|
41
|
+
const loaded = loadRunManifestById(cwd, runId);
|
|
42
|
+
if (!loaded) throw new Error(`Run '${runId}' not found.`);
|
|
43
|
+
let { manifest, tasks } = loaded;
|
|
44
|
+
appendEvent(manifest.eventsPath, { type: "async.started", runId: manifest.runId, data: { pid: process.pid } });
|
|
45
|
+
writeAsyncStartMarker(manifest, { pid: process.pid, startedAt: new Date().toISOString() });
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const agents = allAgents(discoverAgents(cwd));
|
|
49
|
+
const direct = directTeamAndWorkflowFromRun(manifest, tasks, agents);
|
|
50
|
+
const team = direct?.team ?? allTeams(discoverTeams(cwd)).find((candidate) => candidate.name === manifest.team);
|
|
51
|
+
if (!team) throw new Error(`Team '${manifest.team}' not found.`);
|
|
52
|
+
const baseWorkflow = direct?.workflow ?? allWorkflows(discoverWorkflows(cwd)).find((candidate) => candidate.name === manifest.workflow);
|
|
53
|
+
if (!baseWorkflow) throw new Error(`Workflow '${manifest.workflow ?? ""}' not found.`);
|
|
54
|
+
const workflow = expandParallelResearchWorkflow(baseWorkflow, cwd);
|
|
55
|
+
const loadedConfig = loadConfig(cwd);
|
|
56
|
+
const runConfig = manifest.runConfig && typeof manifest.runConfig === "object" && !Array.isArray(manifest.runConfig) ? manifest.runConfig as typeof loadedConfig.config : loadedConfig.config;
|
|
57
|
+
const runtime = manifest.runtimeResolution ? { kind: manifest.runtimeResolution.kind, requestedMode: manifest.runtimeResolution.requestedMode, available: manifest.runtimeResolution.available, fallback: manifest.runtimeResolution.fallback, steer: manifest.runtimeResolution.kind === "live-session", resume: manifest.runtimeResolution.kind === "live-session", liveToolActivity: manifest.runtimeResolution.kind === "live-session", transcript: manifest.runtimeResolution.kind !== "scaffold", reason: manifest.runtimeResolution.reason, safety: manifest.runtimeResolution.safety } : await resolveCrewRuntime(runConfig);
|
|
58
|
+
const runtimeResolution = manifest.runtimeResolution ?? runtimeResolutionState(runtime);
|
|
59
|
+
manifest = { ...manifest, runtimeResolution, runConfig, updatedAt: new Date().toISOString() };
|
|
60
|
+
saveRunManifest(manifest);
|
|
61
|
+
appendEvent(manifest.eventsPath, { type: "runtime.resolved", runId: manifest.runId, message: `Runtime resolved: ${runtime.kind} safety=${runtime.safety}`, data: { runtimeResolution, async: true } });
|
|
62
|
+
if (runtime.safety === "blocked") throw new Error(runtime.reason ?? "Child worker execution is disabled; refusing to create no-op scaffold subagents.");
|
|
63
|
+
const executeWorkers = runtime.kind !== "scaffold";
|
|
64
|
+
const result = await executeTeamRun({ manifest, tasks, team, workflow, agents, executeWorkers, limits: runConfig.limits, runtime, runtimeConfig: runConfig.runtime, skillOverride: manifest.skillOverride, reliability: runConfig.reliability });
|
|
65
|
+
manifest = result.manifest;
|
|
66
|
+
tasks = result.tasks;
|
|
67
|
+
appendEvent(manifest.eventsPath, { type: "async.completed", runId: manifest.runId, data: { status: manifest.status, tasks: tasks.length } });
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
70
|
+
manifest = updateRunStatus(manifest, "failed", message);
|
|
71
|
+
appendEvent(manifest.eventsPath, { type: "async.failed", runId: manifest.runId, message });
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
} finally {
|
|
74
|
+
stopParentGuard();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await main();
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { CrewCancellationError, type CancellationReason, cancellationReasonFromUnknown } from "./cancellation.ts";
|
|
2
|
+
|
|
3
|
+
export interface CancellationTokenState {
|
|
4
|
+
aborted: boolean;
|
|
5
|
+
reason?: CancellationReason;
|
|
6
|
+
lastHeartbeatAt?: string;
|
|
7
|
+
lastHeartbeatStage?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CancellationTokenOptions {
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
onHeartbeat?: (state: CancellationTokenState) => void;
|
|
13
|
+
now?: () => Date;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class CancellationToken {
|
|
17
|
+
readonly #controller = new AbortController();
|
|
18
|
+
readonly #onHeartbeat?: (state: CancellationTokenState) => void;
|
|
19
|
+
readonly #now: () => Date;
|
|
20
|
+
#reason?: CancellationReason;
|
|
21
|
+
#lastHeartbeatAt?: string;
|
|
22
|
+
#lastHeartbeatStage?: string;
|
|
23
|
+
|
|
24
|
+
constructor(options: CancellationTokenOptions = {}) {
|
|
25
|
+
this.#onHeartbeat = options.onHeartbeat;
|
|
26
|
+
this.#now = options.now ?? (() => new Date());
|
|
27
|
+
if (options.signal?.aborted) this.abort(options.signal.reason);
|
|
28
|
+
else if (options.signal) options.signal.addEventListener("abort", () => this.abort(options.signal?.reason), { once: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get signal(): AbortSignal { return this.#controller.signal; }
|
|
32
|
+
get aborted(): boolean { return this.#controller.signal.aborted; }
|
|
33
|
+
get reason(): CancellationReason | undefined { return this.#reason; }
|
|
34
|
+
get lastHeartbeatAt(): string | undefined { return this.#lastHeartbeatAt; }
|
|
35
|
+
get lastHeartbeatStage(): string | undefined { return this.#lastHeartbeatStage; }
|
|
36
|
+
|
|
37
|
+
heartbeat(stage?: string): CancellationTokenState {
|
|
38
|
+
this.throwIfCancelled();
|
|
39
|
+
this.#lastHeartbeatAt = this.#now().toISOString();
|
|
40
|
+
this.#lastHeartbeatStage = stage;
|
|
41
|
+
const state = this.state();
|
|
42
|
+
this.#onHeartbeat?.(state);
|
|
43
|
+
return state;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
throwIfCancelled(): void {
|
|
47
|
+
if (this.aborted) throw new CrewCancellationError(this.#reason ?? cancellationReasonFromUnknown(this.#controller.signal.reason));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
abort(reason?: unknown): void {
|
|
51
|
+
if (this.aborted) return;
|
|
52
|
+
this.#reason = cancellationReasonFromUnknown(reason);
|
|
53
|
+
this.#controller.abort(this.#reason);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
wait(ms: number): Promise<void> {
|
|
57
|
+
this.throwIfCancelled();
|
|
58
|
+
if (ms <= 0) return Promise.resolve();
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
61
|
+
const cleanup = (): void => {
|
|
62
|
+
if (timeout) clearTimeout(timeout);
|
|
63
|
+
this.signal.removeEventListener("abort", onAbort);
|
|
64
|
+
};
|
|
65
|
+
const onAbort = (): void => {
|
|
66
|
+
cleanup();
|
|
67
|
+
reject(new CrewCancellationError(this.#reason ?? cancellationReasonFromUnknown(this.signal.reason)));
|
|
68
|
+
};
|
|
69
|
+
timeout = setTimeout(() => {
|
|
70
|
+
cleanup();
|
|
71
|
+
resolve();
|
|
72
|
+
}, ms);
|
|
73
|
+
this.signal.addEventListener("abort", onAbort, { once: true });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
state(): CancellationTokenState {
|
|
78
|
+
return {
|
|
79
|
+
aborted: this.aborted,
|
|
80
|
+
...(this.#reason ? { reason: this.#reason } : {}),
|
|
81
|
+
...(this.#lastHeartbeatAt ? { lastHeartbeatAt: this.#lastHeartbeatAt } : {}),
|
|
82
|
+
...(this.#lastHeartbeatStage ? { lastHeartbeatStage: this.#lastHeartbeatStage } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createCancellationToken(options: CancellationTokenOptions = {}): CancellationToken {
|
|
88
|
+
return new CancellationToken(options);
|
|
89
|
+
}
|