pi-crew 0.1.45 → 0.1.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/agents/analyst.md +1 -1
- package/agents/critic.md +1 -1
- package/agents/executor.md +1 -1
- package/agents/explorer.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/agents/writer.md +1 -1
- package/docs/next-upgrade-roadmap.md +733 -0
- package/docs/refactor-tasks-phase3.md +394 -394
- package/docs/refactor-tasks-phase4.md +564 -564
- package/docs/refactor-tasks-phase5.md +402 -402
- package/docs/refactor-tasks-phase6.md +662 -662
- package/docs/research-awesome-agent-skills-distillation.md +100 -0
- package/docs/research-extension-examples.md +297 -297
- package/docs/research-extension-system.md +324 -324
- package/docs/research-oh-my-pi-distillation.md +322 -0
- package/docs/research-optimization-plan.md +548 -548
- package/docs/research-phase10-distillation.md +198 -198
- package/docs/research-phase11-distillation.md +201 -201
- package/docs/research-pi-coding-agent.md +357 -357
- package/docs/research-source-pi-crew-reference.md +174 -174
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +107 -83
- package/docs/usage.md +3 -3
- package/index.ts +6 -6
- package/install.mjs +52 -8
- package/package.json +1 -1
- package/schema.json +2 -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/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-serializer.ts +34 -34
- package/src/agents/discover-agents.ts +12 -11
- package/src/config/config.ts +48 -24
- package/src/config/defaults.ts +14 -0
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/project-init.ts +62 -2
- package/src/extension/register.ts +11 -9
- package/src/extension/registration/commands.ts +32 -25
- package/src/extension/registration/compaction-guard.ts +125 -125
- 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 +8 -6
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-index.ts +13 -5
- package/src/extension/run-maintenance.ts +62 -43
- package/src/extension/team-tool/api.ts +25 -8
- package/src/extension/team-tool/cancel.ts +33 -4
- package/src/extension/team-tool/context.ts +5 -0
- package/src/extension/team-tool/handle-settings.ts +188 -188
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +91 -79
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/respond.ts +37 -17
- package/src/extension/team-tool/run.ts +52 -10
- package/src/extension/team-tool/status.ts +12 -1
- package/src/extension/team-tool-types.ts +2 -0
- package/src/extension/team-tool.ts +32 -11
- package/src/i18n.ts +184 -184
- package/src/observability/event-to-metric.ts +8 -1
- package/src/observability/exporters/otlp-exporter.ts +77 -77
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/agent-control.ts +63 -63
- 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/attention-events.ts +28 -28
- package/src/runtime/background-runner.ts +59 -53
- package/src/runtime/cancellation.ts +51 -0
- package/src/runtime/child-pi.ts +457 -444
- package/src/runtime/completion-guard.ts +190 -190
- package/src/runtime/crash-recovery.ts +1 -0
- package/src/runtime/crew-agent-records.ts +38 -6
- package/src/runtime/deadletter.ts +1 -0
- package/src/runtime/delivery-coordinator.ts +46 -25
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/effectiveness.ts +76 -0
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +106 -106
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +124 -124
- package/src/runtime/live-agent-control.ts +88 -87
- package/src/runtime/live-agent-manager.ts +103 -85
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +309 -305
- package/src/runtime/manifest-cache.ts +17 -2
- package/src/runtime/model-fallback.ts +6 -4
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/pi-args.ts +18 -3
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/process-status.ts +5 -1
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +81 -64
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/runtime-resolver.ts +22 -6
- package/src/runtime/session-resources.ts +25 -25
- package/src/runtime/session-snapshot.ts +59 -59
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/skill-instructions.ts +222 -0
- package/src/runtime/stale-reconciler.ts +4 -14
- package/src/runtime/subagent-manager.ts +3 -0
- package/src/runtime/supervisor-contact.ts +59 -59
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +127 -127
- package/src/runtime/task-runner/capabilities.ts +78 -0
- package/src/runtime/task-runner/live-executor.ts +105 -101
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/prompt-builder.ts +3 -1
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -0
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/task-runner.ts +44 -5
- package/src/runtime/team-runner.ts +78 -15
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +1 -0
- package/src/schema/team-tool-schema.ts +3 -3
- package/src/state/active-run-registry.ts +165 -0
- package/src/state/contracts.ts +1 -1
- package/src/state/mailbox.ts +44 -4
- package/src/state/state-store.ts +8 -1
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +44 -2
- 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/team-config.ts +1 -0
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/crew-widget.ts +4 -3
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dashboard-panes/progress-pane.ts +2 -0
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/loaders.ts +158 -158
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/render-scheduler.ts +143 -143
- package/src/ui/run-snapshot-cache.ts +10 -2
- package/src/ui/snapshot-types.ts +2 -0
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +58 -58
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/utils/atomic-write.ts +33 -33
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/frontmatter.ts +68 -68
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/names.ts +27 -27
- package/src/utils/paths.ts +4 -2
- package/src/utils/redaction.ts +44 -44
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/sleep.ts +32 -32
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/workflows/workflow-config.ts +1 -0
- package/src/worktree/branch-freshness.ts +45 -45
- 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/workflows/default.workflow.md +29 -29
- package/workflows/fast-fix.workflow.md +22 -22
- package/workflows/implementation.workflow.md +38 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { closeWatcher, watchWithErrorHandler } from "../utils/fs-watch.ts";
|
|
4
4
|
import { findRepoRoot, projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
|
|
5
|
+
import { activeRunEntries } from "../state/active-run-registry.ts";
|
|
5
6
|
import { isSafePathId, resolveContainedRelativePath, resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
6
7
|
import type { TeamRunManifest } from "../state/types.ts";
|
|
7
8
|
import { DEFAULT_CACHE, DEFAULT_PATHS } from "../config/defaults.ts";
|
|
@@ -106,8 +107,10 @@ function parseManifestIfChanged(root: string, runId: string, filePath: string, p
|
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
function listRunRoots(cwd: string): string[] {
|
|
110
|
+
const roots = new Set<string>();
|
|
109
111
|
const base = findRepoRoot(cwd) ? projectCrewRoot(cwd) : userCrewRoot();
|
|
110
|
-
|
|
112
|
+
roots.add(path.join(base, DEFAULT_PATHS.state.runsSubdir));
|
|
113
|
+
return [...roots];
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
function collectRoots(root: string): ParsedEntry[] {
|
|
@@ -156,6 +159,15 @@ export function createManifestCache(cwd: string, options: ManifestCacheOptions =
|
|
|
156
159
|
function loadManifest(runId: string, rootsToCheck: string[]): CachedManifest | undefined {
|
|
157
160
|
let cached = manifestIndex.get(runId);
|
|
158
161
|
if (!isSafePathId(runId)) return undefined;
|
|
162
|
+
const activeEntry = activeRunEntries().find((entry) => entry.runId === runId);
|
|
163
|
+
if (activeEntry) {
|
|
164
|
+
const activeRoot = path.dirname(activeEntry.stateRoot);
|
|
165
|
+
const parsed = parseManifestIfChanged(activeRoot, runId, activeEntry.manifestPath, cached);
|
|
166
|
+
if (parsed) {
|
|
167
|
+
manifestIndex.set(runId, parsed);
|
|
168
|
+
return parsed;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
159
171
|
for (const root of rootsToCheck) {
|
|
160
172
|
const manifestPath = manifestPathForRun(root, runId);
|
|
161
173
|
if (!manifestPath) continue;
|
|
@@ -180,7 +192,10 @@ export function createManifestCache(cwd: string, options: ManifestCacheOptions =
|
|
|
180
192
|
if (cached && cached.expireAtMs > now) {
|
|
181
193
|
return cached.runs;
|
|
182
194
|
}
|
|
183
|
-
const parsedEntries =
|
|
195
|
+
const parsedEntries = [
|
|
196
|
+
...roots.flatMap((root) => collectRoots(root)),
|
|
197
|
+
...activeRunEntries().map((entry) => ({ runId: entry.runId, path: entry.manifestPath })),
|
|
198
|
+
];
|
|
184
199
|
const unique = new Map<string, CachedManifest | undefined>();
|
|
185
200
|
for (const entry of parsedEntries) {
|
|
186
201
|
if (entry.runId.length === 0) continue;
|
|
@@ -117,9 +117,10 @@ export function configuredModelInfosFromPiConfig(cwd?: string): AvailableModelIn
|
|
|
117
117
|
const globalSettings = readJsonObject(path.join(agentDir, "settings.json")) as PiSettingsLike | undefined;
|
|
118
118
|
const projectSettings = cwd ? readJsonObject(path.join(cwd, ".pi", "settings.json")) as PiSettingsLike | undefined : undefined;
|
|
119
119
|
const effectiveSettings = { ...(globalSettings ?? {}), ...(projectSettings ?? {}) };
|
|
120
|
+
const defaultModel = settingsModelInfo(effectiveSettings);
|
|
120
121
|
return uniqueModelInfos([
|
|
122
|
+
...(defaultModel ? [defaultModel] : []),
|
|
121
123
|
...modelsJsonInfos(readJsonObject(path.join(agentDir, "models.json")) as PiModelsJsonLike | undefined),
|
|
122
|
-
...(settingsModelInfo(effectiveSettings) ? [settingsModelInfo(effectiveSettings)!] : []),
|
|
123
124
|
]);
|
|
124
125
|
}
|
|
125
126
|
|
|
@@ -236,6 +237,7 @@ export interface ConfiguredModelRouting {
|
|
|
236
237
|
export function buildConfiguredModelRouting(input: {
|
|
237
238
|
overrideModel?: string;
|
|
238
239
|
stepModel?: string;
|
|
240
|
+
teamRoleModel?: string;
|
|
239
241
|
agentModel?: string;
|
|
240
242
|
fallbackModels?: string[];
|
|
241
243
|
parentModel?: unknown;
|
|
@@ -250,11 +252,11 @@ export function buildConfiguredModelRouting(input: {
|
|
|
250
252
|
// B3: Parent model inheritance — when agent has no model specified,
|
|
251
253
|
// inherit from parent session model before falling back to defaults.
|
|
252
254
|
const effectiveAgentModel = input.agentModel?.trim() ? input.agentModel : parentModel;
|
|
253
|
-
const requested = [input.overrideModel, input.stepModel, effectiveAgentModel].find((model): model is string => Boolean(model?.trim()));
|
|
255
|
+
const requested = [input.overrideModel, input.stepModel, input.teamRoleModel, effectiveAgentModel].find((model): model is string => Boolean(model?.trim()));
|
|
254
256
|
if (availableModels && availableModels.length === 0) return { requested, candidates: [], reason: "no configured Pi models available" };
|
|
255
257
|
const rawModels = availableModels
|
|
256
|
-
? [input.overrideModel, input.stepModel, effectiveAgentModel, ...(input.fallbackModels ?? []), ...availableModels.map((model) => model.fullId)]
|
|
257
|
-
: [input.overrideModel, parentModel];
|
|
258
|
+
? [input.overrideModel, input.stepModel, input.teamRoleModel, effectiveAgentModel, ...(input.fallbackModels ?? []), ...availableModels.map((model) => model.fullId)]
|
|
259
|
+
: [input.overrideModel, input.stepModel, input.teamRoleModel, effectiveAgentModel, ...(input.fallbackModels ?? []), parentModel];
|
|
258
260
|
const configuredModels = rawModels
|
|
259
261
|
.filter((model): model is string => Boolean(model?.trim()))
|
|
260
262
|
.filter((model) => isAvailableModel(model.trim(), availableModels));
|
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
|
|
4
|
-
|
|
5
|
-
export function sourcePiProjects(cwd: string): string[] {
|
|
6
|
-
const sourceDir = path.join(cwd, "Source");
|
|
7
|
-
try {
|
|
8
|
-
return fs.readdirSync(sourceDir, { withFileTypes: true })
|
|
9
|
-
.filter((entry) => entry.isDirectory() && entry.name.startsWith("pi-"))
|
|
10
|
-
.map((entry) => `Source/${entry.name}`)
|
|
11
|
-
.sort();
|
|
12
|
-
} catch {
|
|
13
|
-
return [];
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function chunkProjects(projects: string[], target = 6): string[][] {
|
|
18
|
-
const chunks = Array.from({ length: Math.min(Math.max(1, target), Math.max(1, projects.length)) }, () => [] as string[]);
|
|
19
|
-
projects.forEach((project, index) => chunks[index % chunks.length]!.push(project));
|
|
20
|
-
return chunks.filter((chunk) => chunk.length > 0);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function expandParallelResearchWorkflow(workflow: WorkflowConfig, cwd: string): WorkflowConfig {
|
|
24
|
-
if (workflow.name !== "parallel-research") return workflow;
|
|
25
|
-
const projects = sourcePiProjects(cwd);
|
|
26
|
-
if (projects.length === 0) return workflow;
|
|
27
|
-
const chunks = chunkProjects(projects, Math.min(8, Math.max(4, Math.ceil(projects.length / 3))));
|
|
28
|
-
const exploreSteps: WorkflowStep[] = chunks.map((paths, index) => ({
|
|
29
|
-
id: `explore-shard-${index + 1}`,
|
|
30
|
-
role: "explorer",
|
|
31
|
-
parallelGroup: "explore",
|
|
32
|
-
reads: paths,
|
|
33
|
-
task: [`Explore this dynamic shard for: {goal}`, "", "Paths:", ...paths.map((item) => `- ${item}`), "", "Focus on purpose, architecture, runtime/UI patterns, package config, docs, and lessons for pi-crew."].join("\n"),
|
|
34
|
-
}));
|
|
35
|
-
return {
|
|
36
|
-
...workflow,
|
|
37
|
-
steps: [
|
|
38
|
-
{ id: "discover", role: "explorer", parallelGroup: "inventory", task: `Quickly inventory and validate ${projects.length} pi-* projects for: {goal}\n\nProjects:\n${projects.map((item) => `- ${item}`).join("\n")}\n\nDo not block shard work; summarize routing notes only.` },
|
|
39
|
-
...exploreSteps,
|
|
40
|
-
{ id: "synthesize", role: "analyst", dependsOn: exploreSteps.map((step) => step.id), task: "Synthesize all dynamic shard findings. Identify common patterns, gaps, and concrete recommendations. Use discover output if available, but prioritize completed shard outputs." },
|
|
41
|
-
{ id: "write", role: "writer", dependsOn: ["synthesize"], output: "research-summary.md", task: "Write a concise final summary with evidence, risks, and actionable next steps." },
|
|
42
|
-
],
|
|
43
|
-
};
|
|
44
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
|
|
4
|
+
|
|
5
|
+
export function sourcePiProjects(cwd: string): string[] {
|
|
6
|
+
const sourceDir = path.join(cwd, "Source");
|
|
7
|
+
try {
|
|
8
|
+
return fs.readdirSync(sourceDir, { withFileTypes: true })
|
|
9
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith("pi-"))
|
|
10
|
+
.map((entry) => `Source/${entry.name}`)
|
|
11
|
+
.sort();
|
|
12
|
+
} catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function chunkProjects(projects: string[], target = 6): string[][] {
|
|
18
|
+
const chunks = Array.from({ length: Math.min(Math.max(1, target), Math.max(1, projects.length)) }, () => [] as string[]);
|
|
19
|
+
projects.forEach((project, index) => chunks[index % chunks.length]!.push(project));
|
|
20
|
+
return chunks.filter((chunk) => chunk.length > 0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function expandParallelResearchWorkflow(workflow: WorkflowConfig, cwd: string): WorkflowConfig {
|
|
24
|
+
if (workflow.name !== "parallel-research") return workflow;
|
|
25
|
+
const projects = sourcePiProjects(cwd);
|
|
26
|
+
if (projects.length === 0) return workflow;
|
|
27
|
+
const chunks = chunkProjects(projects, Math.min(8, Math.max(4, Math.ceil(projects.length / 3))));
|
|
28
|
+
const exploreSteps: WorkflowStep[] = chunks.map((paths, index) => ({
|
|
29
|
+
id: `explore-shard-${index + 1}`,
|
|
30
|
+
role: "explorer",
|
|
31
|
+
parallelGroup: "explore",
|
|
32
|
+
reads: paths,
|
|
33
|
+
task: [`Explore this dynamic shard for: {goal}`, "", "Paths:", ...paths.map((item) => `- ${item}`), "", "Focus on purpose, architecture, runtime/UI patterns, package config, docs, and lessons for pi-crew."].join("\n"),
|
|
34
|
+
}));
|
|
35
|
+
return {
|
|
36
|
+
...workflow,
|
|
37
|
+
steps: [
|
|
38
|
+
{ id: "discover", role: "explorer", parallelGroup: "inventory", task: `Quickly inventory and validate ${projects.length} pi-* projects for: {goal}\n\nProjects:\n${projects.map((item) => `- ${item}`).join("\n")}\n\nDo not block shard work; summarize routing notes only.` },
|
|
39
|
+
...exploreSteps,
|
|
40
|
+
{ id: "synthesize", role: "analyst", dependsOn: exploreSteps.map((step) => step.id), task: "Synthesize all dynamic shard findings. Identify common patterns, gaps, and concrete recommendations. Use discover output if available, but prioritize completed shard outputs." },
|
|
41
|
+
{ id: "write", role: "writer", dependsOn: ["synthesize"], output: "research-summary.md", task: "Write a concise final summary with evidence, risks, and actionable next steps." },
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
package/src/runtime/pi-args.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface BuildPiWorkerArgsInput {
|
|
|
15
15
|
model?: string;
|
|
16
16
|
sessionEnabled?: boolean;
|
|
17
17
|
maxDepth?: number;
|
|
18
|
+
skillPaths?: string[];
|
|
18
19
|
env?: NodeJS.ProcessEnv;
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -24,10 +25,16 @@ export interface BuildPiWorkerArgsResult {
|
|
|
24
25
|
tempDir?: string;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
function isValidThinkingLevel(value: string | undefined): value is string {
|
|
29
|
+
return value !== undefined && THINKING_LEVELS.includes(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
export function applyThinkingSuffix(model: string | undefined, thinking: string | undefined): string | undefined {
|
|
28
33
|
if (!model || !thinking || thinking === "off") return model;
|
|
29
34
|
const colonIdx = model.lastIndexOf(":");
|
|
30
|
-
if (colonIdx !== -1 &&
|
|
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;
|
|
31
38
|
return `${model}:${thinking}`;
|
|
32
39
|
}
|
|
33
40
|
|
|
@@ -54,8 +61,15 @@ export function buildPiWorkerArgs(input: BuildPiWorkerArgsInput): BuildPiWorkerA
|
|
|
54
61
|
const args = ["--mode", "json", "-p"];
|
|
55
62
|
if (input.sessionEnabled === false) args.push("--no-session");
|
|
56
63
|
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
64
|
+
const resolvedModel = input.model ?? input.agent.model;
|
|
65
|
+
if (resolvedModel) {
|
|
66
|
+
const modelWithThinking = applyThinkingSuffix(resolvedModel, input.agent.thinking);
|
|
67
|
+
if (modelWithThinking) args.push("--model", modelWithThinking);
|
|
68
|
+
}
|
|
69
|
+
// When no model resolved, pass thinking separately so Pi can apply it to the inherited parent model.
|
|
70
|
+
if (!resolvedModel && input.agent.thinking && input.agent.thinking !== "off" && isValidThinkingLevel(input.agent.thinking)) {
|
|
71
|
+
args.push("--thinking", input.agent.thinking);
|
|
72
|
+
}
|
|
59
73
|
|
|
60
74
|
if (input.agent.tools?.length) args.push("--tools", input.agent.tools.join(","));
|
|
61
75
|
if (input.agent.extensions !== undefined) {
|
|
@@ -65,6 +79,7 @@ export function buildPiWorkerArgs(input: BuildPiWorkerArgsInput): BuildPiWorkerA
|
|
|
65
79
|
args.push("--extension", PROMPT_RUNTIME_EXTENSION_PATH);
|
|
66
80
|
}
|
|
67
81
|
if (!input.agent.inheritSkills) args.push("--no-skills");
|
|
82
|
+
for (const skillPath of input.skillPaths ?? []) args.push("--skill", skillPath);
|
|
68
83
|
|
|
69
84
|
let tempDir: string | undefined;
|
|
70
85
|
if (input.agent.systemPrompt) {
|
|
@@ -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
|
+
}
|
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
import type { CrewLimitsConfig } from "../config/config.ts";
|
|
2
|
-
import type { PolicyDecision, PolicyDecisionAction, PolicyDecisionReason, TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
3
|
-
import { evaluateGreenContract } from "./green-contract.ts";
|
|
4
|
-
import { isWorkerHeartbeatStale } from "./worker-heartbeat.ts";
|
|
5
|
-
|
|
6
|
-
export interface PolicyEngineInput {
|
|
7
|
-
manifest: TeamRunManifest;
|
|
8
|
-
tasks: TeamTaskState[];
|
|
9
|
-
limits?: CrewLimitsConfig;
|
|
10
|
-
now?: Date;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function decision(action: PolicyDecisionAction, reason: PolicyDecisionReason, message: string, taskId?: string): PolicyDecision {
|
|
14
|
-
return {
|
|
15
|
-
action,
|
|
16
|
-
reason,
|
|
17
|
-
message,
|
|
18
|
-
taskId,
|
|
19
|
-
createdAt: new Date().toISOString(),
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function taskDepth(task: TeamTaskState, tasksById: Map<string, TeamTaskState>): number {
|
|
24
|
-
let depth = 0;
|
|
25
|
-
let current = task.graph?.parentId;
|
|
26
|
-
const seen = new Set<string>();
|
|
27
|
-
while (current && !seen.has(current)) {
|
|
28
|
-
seen.add(current);
|
|
29
|
-
depth += 1;
|
|
30
|
-
current = tasksById.get(current)?.graph?.parentId;
|
|
31
|
-
}
|
|
32
|
-
return depth;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function evaluateCrewPolicy(input: PolicyEngineInput): PolicyDecision[] {
|
|
36
|
-
const decisions: PolicyDecision[] = [];
|
|
37
|
-
const maxTasksPerRun = Number.isFinite(input.limits?.maxTasksPerRun) ? input.limits!.maxTasksPerRun : undefined;
|
|
38
|
-
if (maxTasksPerRun !== undefined && input.tasks.length > maxTasksPerRun) {
|
|
39
|
-
decisions.push(decision("block", "limit_exceeded", `Run has ${input.tasks.length} tasks, exceeding maxTasksPerRun=${maxTasksPerRun}.`));
|
|
40
|
-
}
|
|
41
|
-
const runningCount = input.tasks.filter((task) => task.status === "running").length;
|
|
42
|
-
const maxConcurrentWorkers = Number.isFinite(input.limits?.maxConcurrentWorkers) ? input.limits!.maxConcurrentWorkers : undefined;
|
|
43
|
-
if (maxConcurrentWorkers !== undefined && runningCount > maxConcurrentWorkers) {
|
|
44
|
-
decisions.push(decision("block", "limit_exceeded", `Run has ${runningCount} running workers, exceeding maxConcurrentWorkers=${maxConcurrentWorkers}.`));
|
|
45
|
-
}
|
|
46
|
-
const tasksById = new Map(input.tasks.map((task) => [task.id, task]));
|
|
47
|
-
|
|
48
|
-
for (const task of input.tasks) {
|
|
49
|
-
if (input.limits?.maxChildrenPerTask !== undefined && (task.graph?.children.length ?? 0) > input.limits.maxChildrenPerTask) {
|
|
50
|
-
decisions.push(decision("block", "limit_exceeded", `Task has ${task.graph?.children.length ?? 0} children, exceeding maxChildrenPerTask=${input.limits.maxChildrenPerTask}.`, task.id));
|
|
51
|
-
}
|
|
52
|
-
if (input.limits?.maxTaskDepth !== undefined && taskDepth(task, tasksById) > input.limits.maxTaskDepth) {
|
|
53
|
-
decisions.push(decision("block", "limit_exceeded", `Task graph depth exceeds maxTaskDepth=${input.limits.maxTaskDepth}.`, task.id));
|
|
54
|
-
}
|
|
55
|
-
if (task.status === "failed") {
|
|
56
|
-
const retryCount = task.policy?.retryCount ?? 0;
|
|
57
|
-
const maxRetries = input.limits?.maxRetriesPerTask ?? 0;
|
|
58
|
-
decisions.push(decision(retryCount < maxRetries ? "retry" : "escalate", "task_failed", task.error ? `Task failed: ${task.error}` : "Task failed.", task.id));
|
|
59
|
-
}
|
|
60
|
-
if ((task.status === "running" || task.status === "queued") && task.heartbeat && task.heartbeat.alive !== false && isWorkerHeartbeatStale(task.heartbeat, input.limits?.heartbeatStaleMs ?? 60_000, input.now)) {
|
|
61
|
-
decisions.push(decision("escalate", "worker_stale", "Worker heartbeat is stale.", task.id));
|
|
62
|
-
}
|
|
63
|
-
if (task.taskPacket?.verification) {
|
|
64
|
-
const outcome = evaluateGreenContract(task.taskPacket.verification, task.verification);
|
|
65
|
-
if (!outcome.satisfied && task.status === "completed") {
|
|
66
|
-
decisions.push(decision("block", "green_unsatisfied", `Green contract unsatisfied: required=${outcome.requiredGreenLevel}, observed=${outcome.observedGreenLevel}.`, task.id));
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (decisions.length === 0 && input.tasks.length > 0 && input.tasks.every((task) => task.status === "completed")) {
|
|
72
|
-
decisions.push(decision("closeout", "run_complete", "All tasks completed and no policy blockers were found."));
|
|
73
|
-
}
|
|
74
|
-
return decisions;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function summarizePolicyDecisions(decisions: PolicyDecision[]): string[] {
|
|
78
|
-
return decisions.map((item) => `- ${item.action} (${item.reason})${item.taskId ? ` ${item.taskId}` : ""}: ${item.message}`);
|
|
79
|
-
}
|
|
1
|
+
import type { CrewLimitsConfig } from "../config/config.ts";
|
|
2
|
+
import type { PolicyDecision, PolicyDecisionAction, PolicyDecisionReason, TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
3
|
+
import { evaluateGreenContract } from "./green-contract.ts";
|
|
4
|
+
import { isWorkerHeartbeatStale } from "./worker-heartbeat.ts";
|
|
5
|
+
|
|
6
|
+
export interface PolicyEngineInput {
|
|
7
|
+
manifest: TeamRunManifest;
|
|
8
|
+
tasks: TeamTaskState[];
|
|
9
|
+
limits?: CrewLimitsConfig;
|
|
10
|
+
now?: Date;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function decision(action: PolicyDecisionAction, reason: PolicyDecisionReason, message: string, taskId?: string): PolicyDecision {
|
|
14
|
+
return {
|
|
15
|
+
action,
|
|
16
|
+
reason,
|
|
17
|
+
message,
|
|
18
|
+
taskId,
|
|
19
|
+
createdAt: new Date().toISOString(),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function taskDepth(task: TeamTaskState, tasksById: Map<string, TeamTaskState>): number {
|
|
24
|
+
let depth = 0;
|
|
25
|
+
let current = task.graph?.parentId;
|
|
26
|
+
const seen = new Set<string>();
|
|
27
|
+
while (current && !seen.has(current)) {
|
|
28
|
+
seen.add(current);
|
|
29
|
+
depth += 1;
|
|
30
|
+
current = tasksById.get(current)?.graph?.parentId;
|
|
31
|
+
}
|
|
32
|
+
return depth;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function evaluateCrewPolicy(input: PolicyEngineInput): PolicyDecision[] {
|
|
36
|
+
const decisions: PolicyDecision[] = [];
|
|
37
|
+
const maxTasksPerRun = Number.isFinite(input.limits?.maxTasksPerRun) ? input.limits!.maxTasksPerRun : undefined;
|
|
38
|
+
if (maxTasksPerRun !== undefined && input.tasks.length > maxTasksPerRun) {
|
|
39
|
+
decisions.push(decision("block", "limit_exceeded", `Run has ${input.tasks.length} tasks, exceeding maxTasksPerRun=${maxTasksPerRun}.`));
|
|
40
|
+
}
|
|
41
|
+
const runningCount = input.tasks.filter((task) => task.status === "running").length;
|
|
42
|
+
const maxConcurrentWorkers = Number.isFinite(input.limits?.maxConcurrentWorkers) ? input.limits!.maxConcurrentWorkers : undefined;
|
|
43
|
+
if (maxConcurrentWorkers !== undefined && runningCount > maxConcurrentWorkers) {
|
|
44
|
+
decisions.push(decision("block", "limit_exceeded", `Run has ${runningCount} running workers, exceeding maxConcurrentWorkers=${maxConcurrentWorkers}.`));
|
|
45
|
+
}
|
|
46
|
+
const tasksById = new Map(input.tasks.map((task) => [task.id, task]));
|
|
47
|
+
|
|
48
|
+
for (const task of input.tasks) {
|
|
49
|
+
if (input.limits?.maxChildrenPerTask !== undefined && (task.graph?.children.length ?? 0) > input.limits.maxChildrenPerTask) {
|
|
50
|
+
decisions.push(decision("block", "limit_exceeded", `Task has ${task.graph?.children.length ?? 0} children, exceeding maxChildrenPerTask=${input.limits.maxChildrenPerTask}.`, task.id));
|
|
51
|
+
}
|
|
52
|
+
if (input.limits?.maxTaskDepth !== undefined && taskDepth(task, tasksById) > input.limits.maxTaskDepth) {
|
|
53
|
+
decisions.push(decision("block", "limit_exceeded", `Task graph depth exceeds maxTaskDepth=${input.limits.maxTaskDepth}.`, task.id));
|
|
54
|
+
}
|
|
55
|
+
if (task.status === "failed") {
|
|
56
|
+
const retryCount = task.policy?.retryCount ?? 0;
|
|
57
|
+
const maxRetries = input.limits?.maxRetriesPerTask ?? 0;
|
|
58
|
+
decisions.push(decision(retryCount < maxRetries ? "retry" : "escalate", "task_failed", task.error ? `Task failed: ${task.error}` : "Task failed.", task.id));
|
|
59
|
+
}
|
|
60
|
+
if ((task.status === "running" || task.status === "queued") && task.heartbeat && task.heartbeat.alive !== false && isWorkerHeartbeatStale(task.heartbeat, input.limits?.heartbeatStaleMs ?? 60_000, input.now)) {
|
|
61
|
+
decisions.push(decision("escalate", "worker_stale", "Worker heartbeat is stale.", task.id));
|
|
62
|
+
}
|
|
63
|
+
if (task.taskPacket?.verification) {
|
|
64
|
+
const outcome = evaluateGreenContract(task.taskPacket.verification, task.verification);
|
|
65
|
+
if (!outcome.satisfied && task.status === "completed") {
|
|
66
|
+
decisions.push(decision("block", "green_unsatisfied", `Green contract unsatisfied: required=${outcome.requiredGreenLevel}, observed=${outcome.observedGreenLevel}.`, task.id));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (decisions.length === 0 && input.tasks.length > 0 && input.tasks.every((task) => task.status === "completed")) {
|
|
72
|
+
decisions.push(decision("closeout", "run_complete", "All tasks completed and no policy blockers were found."));
|
|
73
|
+
}
|
|
74
|
+
return decisions;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function summarizePolicyDecisions(decisions: PolicyDecision[]): string[] {
|
|
78
|
+
return decisions.map((item) => `- ${item.action} (${item.reason})${item.taskId ? ` ${item.taskId}` : ""}: ${item.message}`);
|
|
79
|
+
}
|
|
@@ -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
|
}
|