pi-ui-extend 1.0.41 → 1.0.45
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 +50 -13
- package/dist/app/app.d.ts +13 -0
- package/dist/app/app.js +158 -8
- package/dist/app/cli/install.js +2 -2
- package/dist/app/commands/command-controller.d.ts +4 -0
- package/dist/app/commands/command-controller.js +11 -0
- package/dist/app/commands/command-git-actions.d.ts +25 -0
- package/dist/app/commands/command-git-actions.js +381 -0
- package/dist/app/commands/command-host.d.ts +4 -0
- package/dist/app/commands/command-host.js +5 -2
- package/dist/app/commands/command-model-actions.d.ts +1 -0
- package/dist/app/commands/command-model-actions.js +57 -35
- package/dist/app/commands/command-navigation-actions.d.ts +1 -0
- package/dist/app/commands/command-navigation-actions.js +16 -3
- package/dist/app/commands/command-registry.d.ts +2 -0
- package/dist/app/commands/command-registry.js +15 -1
- package/dist/app/commands/command-session-actions.js +9 -3
- package/dist/app/commands/reload-context-inventory.d.ts +11 -0
- package/dist/app/commands/reload-context-inventory.js +70 -0
- package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
- package/dist/app/extensions/extension-actions-controller.js +6 -0
- package/dist/app/extensions/subagent-catalog-state.d.ts +9 -0
- package/dist/app/extensions/subagent-catalog-state.js +23 -0
- package/dist/app/input/autocomplete-controller.js +61 -34
- package/dist/app/input/input-action-controller.d.ts +2 -0
- package/dist/app/input/input-action-controller.js +8 -1
- package/dist/app/input/input-controller.d.ts +2 -1
- package/dist/app/input/input-controller.js +7 -2
- package/dist/app/input/prompt-enhancer-controller.js +53 -35
- package/dist/app/input/voice-controller.d.ts +41 -45
- package/dist/app/input/voice-controller.js +351 -419
- package/dist/app/model/model-usage-controller.js +17 -7
- package/dist/app/model/model-usage-status.d.ts +4 -1
- package/dist/app/model/model-usage-status.js +270 -46
- package/dist/app/popup/menu-items-controller.d.ts +13 -3
- package/dist/app/popup/menu-items-controller.js +37 -21
- package/dist/app/popup/popup-action-controller.d.ts +12 -2
- package/dist/app/popup/popup-action-controller.js +77 -24
- package/dist/app/popup/popup-menu-controller.d.ts +36 -14
- package/dist/app/popup/popup-menu-controller.js +239 -69
- package/dist/app/rendering/dcp-stats.d.ts +9 -4
- package/dist/app/rendering/dcp-stats.js +40 -425
- package/dist/app/rendering/editor-panels.js +10 -4
- package/dist/app/rendering/popup-menu-renderer.d.ts +3 -5
- package/dist/app/rendering/popup-menu-renderer.js +42 -37
- package/dist/app/rendering/render-controller.js +23 -2
- package/dist/app/rendering/status-line-renderer.d.ts +5 -0
- package/dist/app/rendering/status-line-renderer.js +41 -5
- package/dist/app/rendering/tab-line-renderer.js +26 -20
- package/dist/app/runtime.d.ts +12 -1
- package/dist/app/runtime.js +120 -12
- package/dist/app/screen/mouse-controller.d.ts +2 -0
- package/dist/app/screen/mouse-controller.js +17 -7
- package/dist/app/screen/status-controller.d.ts +4 -0
- package/dist/app/screen/status-controller.js +5 -0
- package/dist/app/session/lazy-session-manager.js +34 -0
- package/dist/app/session/session-event-controller.d.ts +1 -0
- package/dist/app/session/session-event-controller.js +10 -1
- package/dist/app/session/session-history.d.ts +1 -0
- package/dist/app/session/session-history.js +12 -1
- package/dist/app/session/session-lifecycle-controller.d.ts +7 -1
- package/dist/app/session/session-lifecycle-controller.js +16 -1
- package/dist/app/session/tabs-controller.d.ts +26 -1
- package/dist/app/session/tabs-controller.js +454 -147
- package/dist/app/subagents/subagents-files.js +60 -1
- package/dist/app/subagents/subagents-model.d.ts +1 -0
- package/dist/app/subagents/subagents-model.js +18 -2
- package/dist/app/subagents/subagents-widget-controller.d.ts +1 -0
- package/dist/app/subagents/subagents-widget-controller.js +6 -0
- package/dist/app/types.d.ts +16 -1
- package/dist/app/workspace/workspace-actions-controller.js +10 -2
- package/dist/app/workspace/workspace-undo.d.ts +1 -0
- package/dist/app/workspace/workspace-undo.js +1 -0
- package/dist/bundled-extensions/question/index.js +9 -1
- package/dist/bundled-extensions/question/remote.d.ts +4 -0
- package/dist/bundled-extensions/question/remote.js +33 -0
- package/dist/bundled-extensions/telegram-connector/bot.d.ts +43 -0
- package/dist/bundled-extensions/telegram-connector/bot.js +166 -0
- package/dist/bundled-extensions/telegram-connector/config.d.ts +8 -0
- package/dist/bundled-extensions/telegram-connector/config.js +87 -0
- package/dist/bundled-extensions/telegram-connector/coordinator.d.ts +66 -0
- package/dist/bundled-extensions/telegram-connector/coordinator.js +413 -0
- package/dist/bundled-extensions/telegram-connector/index.d.ts +3 -0
- package/dist/bundled-extensions/telegram-connector/index.js +195 -0
- package/dist/bundled-extensions/terminal-bell/index.d.ts +0 -8
- package/dist/bundled-extensions/terminal-bell/index.js +0 -76
- package/dist/bundled-extensions/workspace-undo/index.d.ts +26 -0
- package/dist/bundled-extensions/workspace-undo/index.js +191 -0
- package/dist/config.d.ts +21 -2
- package/dist/config.js +159 -32
- package/dist/default-pix-config.js +25 -5
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.js +1 -0
- package/dist/schemas/pi-tools-suite-schema.d.ts +88 -62
- package/dist/schemas/pi-tools-suite-schema.js +54 -83
- package/dist/schemas/pix-schema.d.ts +19 -2
- package/dist/schemas/pix-schema.js +47 -5
- package/dist/schemas/tasks-schema.d.ts +18 -0
- package/dist/schemas/tasks-schema.js +41 -0
- package/docs/concurrency.md +9 -1
- package/docs/desktop-mvp.md +22 -6
- package/docs/desktop-task-manager.md +148 -84
- package/docs/release.md +30 -6
- package/external/pi-tools-suite/README.md +197 -84
- package/external/pi-tools-suite/docs/evals.md +1 -1
- package/external/pi-tools-suite/docs/session-recovery.md +47 -14
- package/external/pi-tools-suite/docs/subagent-model-pools.md +48 -30
- package/external/pi-tools-suite/docs/ui-qa-subagent.md +440 -0
- package/external/pi-tools-suite/package.json +6 -1
- package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +2 -1
- package/external/pi-tools-suite/src/antigravity-auth/constants.ts +9 -3
- package/external/pi-tools-suite/src/antigravity-auth/headers.ts +41 -3
- package/external/pi-tools-suite/src/antigravity-auth/models.ts +94 -21
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +5 -17
- package/external/pi-tools-suite/src/antigravity-auth/payload.ts +72 -7
- package/external/pi-tools-suite/src/antigravity-auth/stream.ts +13 -1
- package/external/pi-tools-suite/src/async-subagents/agents/frontier-review.md +23 -0
- package/external/pi-tools-suite/src/async-subagents/agents/implement.md +1 -1
- package/external/pi-tools-suite/src/async-subagents/agents/presets.jsonc +16 -0
- package/external/pi-tools-suite/src/async-subagents/agents/research.md +5 -3
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/browser.mjs +346 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/desktop.mjs +1038 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/tui.mjs +759 -0
- package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/scripts/browser-qa-runner.mjs +32 -23
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/chrome-devtools/chrome-devtools-provider.mjs +895 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/linux/linux-atspi.py +494 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/macos/macos-accessibility.swift +1085 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/bridge-client.mjs +50 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/native-terminal-host.mjs +501 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/windows/windows-uia.ps1 +454 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-auth.md +114 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-chrome-devtools.md +91 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-playwright.md +145 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser.md +82 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-linux.md +26 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-macos.md +29 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-windows.md +23 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop.md +54 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-native-terminal.md +59 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-pty.md +37 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui.md +60 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/scripts/ui-qa-runner.mjs +535 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa.md +75 -0
- package/external/pi-tools-suite/src/async-subagents/commands.ts +15 -71
- package/external/pi-tools-suite/src/async-subagents/core/activity.ts +33 -0
- package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +5 -4
- package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +8 -5
- package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +16 -2
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +160 -327
- package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +2 -1
- package/external/pi-tools-suite/src/async-subagents/core/registry.ts +158 -33
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +21 -5
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +40 -26
- package/external/pi-tools-suite/src/async-subagents/core/state.ts +40 -0
- package/external/pi-tools-suite/src/async-subagents/core/types.ts +7 -0
- package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +51 -45
- package/external/pi-tools-suite/src/async-subagents/index.ts +69 -5
- package/external/pi-tools-suite/src/async-subagents/lib.ts +16 -9
- package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +8 -5
- package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +5 -4
- package/external/pi-tools-suite/src/async-subagents/types.ts +1 -0
- package/external/pi-tools-suite/src/coding-discipline/index.ts +90 -59
- package/external/pi-tools-suite/src/config.ts +55 -1
- package/external/pi-tools-suite/src/context-gateway/accounting-log.ts +282 -0
- package/external/pi-tools-suite/src/context-gateway/config.ts +77 -3
- package/external/pi-tools-suite/src/context-gateway/efficiency.ts +414 -0
- package/external/pi-tools-suite/src/context-gateway/enforcement.ts +222 -0
- package/external/pi-tools-suite/src/context-gateway/index.ts +206 -19
- package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +7 -7
- package/external/pi-tools-suite/src/context-gateway/telemetry.ts +101 -28
- package/external/pi-tools-suite/src/context-gateway/types.ts +16 -3
- package/external/pi-tools-suite/src/context-inventory.ts +99 -0
- package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +39 -4
- package/external/pi-tools-suite/src/dcp/auto-compress.ts +128 -49
- package/external/pi-tools-suite/src/dcp/commands.ts +32 -156
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +177 -46
- package/external/pi-tools-suite/src/dcp/compression-blocks.ts +6 -58
- package/external/pi-tools-suite/src/dcp/compression-preview.ts +9 -0
- package/external/pi-tools-suite/src/dcp/compression-progress.ts +22 -0
- package/external/pi-tools-suite/src/dcp/config.ts +114 -17
- package/external/pi-tools-suite/src/dcp/conversation-index.ts +36 -9
- package/external/pi-tools-suite/src/dcp/diagnostics.ts +41 -0
- package/external/pi-tools-suite/src/dcp/fresh-tool-results.ts +38 -0
- package/external/pi-tools-suite/src/dcp/index.ts +338 -66
- package/external/pi-tools-suite/src/dcp/journal.ts +126 -4
- package/external/pi-tools-suite/src/dcp/progress-controller.ts +2 -1
- package/external/pi-tools-suite/src/dcp/prompts.ts +83 -192
- package/external/pi-tools-suite/src/dcp/protected-continuity.ts +175 -0
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +93 -3
- package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +6 -10
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +36 -33
- package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -3
- package/external/pi-tools-suite/src/dcp/pruner.ts +1 -0
- package/external/pi-tools-suite/src/dcp/routine-pressure.ts +86 -0
- package/external/pi-tools-suite/src/dcp/state.ts +9 -0
- package/external/pi-tools-suite/src/dcp/statistics.d.ts +8 -0
- package/external/pi-tools-suite/src/dcp/statistics.js +156 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +78 -22
- package/external/pi-tools-suite/src/index.ts +7 -1
- package/external/pi-tools-suite/src/lib/project.ts +36 -1
- package/external/pi-tools-suite/src/model-tools/index.ts +10 -7
- package/external/pi-tools-suite/src/repo-discovery/index.ts +304 -4
- package/external/pi-tools-suite/src/resource-registry/index.ts +2551 -0
- package/external/pi-tools-suite/src/session-recovery/index.ts +17 -0
- package/external/pi-tools-suite/src/shell-command-policy.ts +219 -0
- package/external/pi-tools-suite/src/todo/index.ts +21 -0
- package/external/pi-tools-suite/src/todo/todo.ts +19 -1
- package/external/pi-tools-suite/src/tool-descriptions.ts +47 -19
- package/package.json +10 -9
- package/schemas/pi-tools-suite.json +466 -287
- package/schemas/pix.json +129 -11
- package/schemas/tasks.json +131 -0
- package/skills/simplify/SKILL.md +33 -5
- package/docs/desktop-markdown-media.md +0 -77
- package/external/pi-tools-suite/docs/browser-qa-subagent.md +0 -177
- package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +0 -102
- package/external/pi-tools-suite/src/async-subagents/agents/browser-qa.md +0 -598
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -54
- package/external/pi-tools-suite/src/skill-installer/index.ts +0 -333
- package/skills/playwright-cli/SKILL.md +0 -420
- package/skills/playwright-cli/references/element-attributes.md +0 -23
- package/skills/playwright-cli/references/playwright-tests.md +0 -50
- package/skills/playwright-cli/references/request-mocking.md +0 -87
- package/skills/playwright-cli/references/running-code.md +0 -241
- package/skills/playwright-cli/references/session-management.md +0 -273
- package/skills/playwright-cli/references/spec-driven-testing.md +0 -311
- package/skills/playwright-cli/references/storage-state.md +0 -290
- package/skills/playwright-cli/references/test-generation.md +0 -142
- package/skills/playwright-cli/references/tracing.md +0 -154
- package/skills/playwright-cli/references/video-recording.md +0 -147
- package/skills/spec-lite/SKILL.md +0 -140
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-auth.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-flow.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.LICENSE +0 -0
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.mjs +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import { isBlindModelRef, SubagentModelSelectionError, type ResolvedAgentTaskConfig, type SubagentConfig } from "./config.js";
|
|
3
|
+
import { isUiQaType } from "./browser-qa.js";
|
|
3
4
|
import { isSessionModelUnavailable } from "./model-fallback.js";
|
|
4
5
|
|
|
5
6
|
export interface SubagentModelRegistry {
|
|
@@ -17,7 +18,7 @@ export async function selectAvailableAgentModels(
|
|
|
17
18
|
signal?: AbortSignal,
|
|
18
19
|
): Promise<ResolvedAgentTaskConfig> {
|
|
19
20
|
if (signal?.aborted) throw new Error("Aborted");
|
|
20
|
-
const needsImages = Boolean(resolved.task.imagePaths?.length) || resolved.task.subagentType
|
|
21
|
+
const needsImages = Boolean(resolved.task.imagePaths?.length) || isUiQaType(resolved.task.subagentType);
|
|
21
22
|
if (!registry && !needsImages) return resolved; // Narrow offline/test callers.
|
|
22
23
|
const candidates = [resolved.task.model, ...resolved.fallbackModels].filter((ref): ref is string => Boolean(ref));
|
|
23
24
|
let registeredAvailable: Set<string> | undefined;
|
|
@@ -4,6 +4,15 @@ import { getRunRoot, hasAgentPrompt, hasLaunchedAgentPrompt, isDir, resolveRunDi
|
|
|
4
4
|
|
|
5
5
|
export const SUBAGENT_REGISTRY_FILE = "registry.json";
|
|
6
6
|
|
|
7
|
+
const REGISTRY_LOCK_RETRY_MS = 10;
|
|
8
|
+
const REGISTRY_LOCK_TIMEOUT_MS = 2_000;
|
|
9
|
+
const REGISTRY_LOCK_STALE_MS = 30_000;
|
|
10
|
+
|
|
11
|
+
interface RegistryLockOwner {
|
|
12
|
+
pid: number;
|
|
13
|
+
token: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
export interface SubagentRegistryRun {
|
|
8
17
|
runId: string;
|
|
9
18
|
runDir: string;
|
|
@@ -62,45 +71,44 @@ export function loadSubagentRegistry(cwd: string): SubagentRegistry {
|
|
|
62
71
|
|
|
63
72
|
export function saveSubagentRegistry(cwd: string, registry: SubagentRegistry): void {
|
|
64
73
|
const registryPath = getSubagentRegistryPath(cwd);
|
|
65
|
-
|
|
66
|
-
fs.writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`, "utf-8");
|
|
74
|
+
withRegistryLock(registryPath, () => writeRegistryAtomically(registryPath, registry));
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
export function recordSubagentRun(cwd: string, runDir: string, agentIds: string[]): SubagentRegistry {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
78
|
+
return updateSubagentRegistry(cwd, (registry) => {
|
|
79
|
+
const resolvedRunDir = path.resolve(runDir);
|
|
80
|
+
const runId = path.basename(resolvedRunDir);
|
|
81
|
+
const now = new Date().toISOString();
|
|
82
|
+
const uniqueAgentIds = [...new Set(agentIds.filter((id) => id.trim().length > 0))];
|
|
83
|
+
const previous = registry.runs[runId];
|
|
84
|
+
registry.runs[runId] = {
|
|
85
|
+
runId,
|
|
86
|
+
runDir: resolvedRunDir,
|
|
87
|
+
agentIds: uniqueAgentIds,
|
|
88
|
+
createdAt: previous?.createdAt ?? now,
|
|
89
|
+
updatedAt: now,
|
|
90
|
+
};
|
|
91
|
+
registry.latestRunId = runId;
|
|
92
|
+
registry.latestRunDir = resolvedRunDir;
|
|
93
|
+
for (const agentId of uniqueAgentIds) {
|
|
94
|
+
registry.agents[agentId] = { agentId, runId, runDir: resolvedRunDir, updatedAt: now };
|
|
95
|
+
}
|
|
96
|
+
return registry;
|
|
97
|
+
});
|
|
90
98
|
}
|
|
91
99
|
|
|
92
100
|
export function removeSubagentRunsFromRegistry(cwd: string, runDirs: string[]): SubagentRegistry {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
return updateSubagentRegistry(cwd, (registry) => {
|
|
102
|
+
const removed = new Set(runDirs.map((runDir) => normalizePath(runDir)));
|
|
103
|
+
for (const [runId, run] of Object.entries(registry.runs)) {
|
|
104
|
+
if (removed.has(normalizePath(run.runDir))) delete registry.runs[runId];
|
|
105
|
+
}
|
|
106
|
+
for (const [agentId, agent] of Object.entries(registry.agents)) {
|
|
107
|
+
if (removed.has(normalizePath(agent.runDir))) delete registry.agents[agentId];
|
|
108
|
+
}
|
|
109
|
+
refreshLatestRun(registry);
|
|
110
|
+
return registry;
|
|
111
|
+
});
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
export function resolveSubagentRunDir(cwd: string, runDir?: string): string {
|
|
@@ -157,6 +165,123 @@ function refreshLatestRun(registry: SubagentRegistry): void {
|
|
|
157
165
|
delete registry.latestRunDir;
|
|
158
166
|
}
|
|
159
167
|
|
|
168
|
+
function updateSubagentRegistry(cwd: string, update: (registry: SubagentRegistry) => SubagentRegistry): SubagentRegistry {
|
|
169
|
+
const registryPath = getSubagentRegistryPath(cwd);
|
|
170
|
+
return withRegistryLock(registryPath, () => {
|
|
171
|
+
const registry = loadSubagentRegistry(cwd);
|
|
172
|
+
const updated = update(registry);
|
|
173
|
+
writeRegistryAtomically(registryPath, updated);
|
|
174
|
+
return updated;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function withRegistryLock<T>(registryPath: string, operation: () => T): T {
|
|
179
|
+
fs.mkdirSync(path.dirname(registryPath), { recursive: true });
|
|
180
|
+
const lockPath = `${registryPath}.lock`;
|
|
181
|
+
const owner = registryLockOwner();
|
|
182
|
+
const deadline = Date.now() + REGISTRY_LOCK_TIMEOUT_MS;
|
|
183
|
+
while (!createRegistryLock(lockPath, owner)) {
|
|
184
|
+
recoverStaleRegistryLock(lockPath, owner);
|
|
185
|
+
if (Date.now() >= deadline) {
|
|
186
|
+
throw new Error(`Timed out waiting for sub-agent registry lock at ${lockPath}.`);
|
|
187
|
+
}
|
|
188
|
+
sleep(REGISTRY_LOCK_RETRY_MS);
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
return operation();
|
|
192
|
+
} finally {
|
|
193
|
+
releaseRegistryLock(lockPath, owner);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function writeRegistryAtomically(registryPath: string, registry: SubagentRegistry): void {
|
|
198
|
+
const temporaryPath = `${registryPath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
199
|
+
try {
|
|
200
|
+
fs.writeFileSync(temporaryPath, `${JSON.stringify(registry, null, 2)}\n`, { encoding: "utf-8", mode: 0o600, flag: "wx" });
|
|
201
|
+
fs.renameSync(temporaryPath, registryPath);
|
|
202
|
+
} finally {
|
|
203
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function registryLockOwner(): RegistryLockOwner {
|
|
208
|
+
return { pid: process.pid, token: `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}` };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function createRegistryLock(lockPath: string, owner: RegistryLockOwner): boolean {
|
|
212
|
+
try {
|
|
213
|
+
fs.writeFileSync(lockPath, JSON.stringify(owner), { encoding: "utf-8", mode: 0o600, flag: "wx" });
|
|
214
|
+
return true;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (errorCode(error) === "EEXIST") return false;
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function recoverStaleRegistryLock(lockPath: string, owner: RegistryLockOwner): void {
|
|
222
|
+
let lockStat: fs.Stats;
|
|
223
|
+
try {
|
|
224
|
+
lockStat = fs.statSync(lockPath);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (errorCode(error) === "ENOENT") return;
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
const existingOwner = readRegistryLockOwner(lockPath);
|
|
230
|
+
const lockAgeMs = Date.now() - lockStat.mtimeMs;
|
|
231
|
+
if (existingOwner && isProcessAlive(existingOwner.pid)) return;
|
|
232
|
+
if (!existingOwner && lockAgeMs <= REGISTRY_LOCK_STALE_MS) return;
|
|
233
|
+
|
|
234
|
+
const quarantinePath = `${lockPath}.stale-${owner.token}`;
|
|
235
|
+
try {
|
|
236
|
+
fs.renameSync(lockPath, quarantinePath);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (errorCode(error) === "ENOENT") return;
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
const movedStat = fs.statSync(quarantinePath);
|
|
243
|
+
if (movedStat.dev !== lockStat.dev || movedStat.ino !== lockStat.ino) return;
|
|
244
|
+
fs.rmSync(quarantinePath, { force: true });
|
|
245
|
+
} finally {
|
|
246
|
+
if (fs.existsSync(quarantinePath) && !fs.existsSync(lockPath)) {
|
|
247
|
+
fs.renameSync(quarantinePath, lockPath);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function releaseRegistryLock(lockPath: string, owner: RegistryLockOwner): void {
|
|
253
|
+
if (readRegistryLockOwner(lockPath)?.token === owner.token) fs.rmSync(lockPath, { force: true });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function readRegistryLockOwner(lockPath: string): RegistryLockOwner | undefined {
|
|
257
|
+
try {
|
|
258
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(lockPath, "utf-8"));
|
|
259
|
+
if (!isRecord(parsed) || typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0 || typeof parsed.token !== "string") return undefined;
|
|
260
|
+
return { pid: parsed.pid, token: parsed.token };
|
|
261
|
+
} catch {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function isProcessAlive(pid: number): boolean {
|
|
267
|
+
try {
|
|
268
|
+
process.kill(pid, 0);
|
|
269
|
+
return true;
|
|
270
|
+
} catch (error) {
|
|
271
|
+
return errorCode(error) !== "ESRCH";
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function errorCode(error: unknown): string | undefined {
|
|
276
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
|
|
277
|
+
? error.code
|
|
278
|
+
: undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function sleep(ms: number): void {
|
|
282
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
283
|
+
}
|
|
284
|
+
|
|
160
285
|
function looksLikeRunDir(runDir: string): boolean {
|
|
161
286
|
if (isDir(path.join(runDir, "prompts"))) return true;
|
|
162
287
|
try {
|
|
@@ -4,6 +4,8 @@ import type { AgentTask } from "./types.js";
|
|
|
4
4
|
import {
|
|
5
5
|
currentModelRef,
|
|
6
6
|
defaultSubagentType,
|
|
7
|
+
filterSubagentConfigForParentModel,
|
|
8
|
+
normalizeSubagentType,
|
|
7
9
|
resolveSubagentRoutingConfig,
|
|
8
10
|
type ResolvedSubagentRoutingConfig,
|
|
9
11
|
type SubagentConfig,
|
|
@@ -45,7 +47,7 @@ export class SubagentRoutingError extends Error {
|
|
|
45
47
|
const ROUTER_SYSTEM_PROMPT = [
|
|
46
48
|
"You route Pi async sub-agent tasks to the best configured subagentType.",
|
|
47
49
|
"Choose exactly one allowed type for each task. Use the allowed type descriptions as the source of truth.",
|
|
48
|
-
"Prefer a matching project specialist. Otherwise use research for reading/review
|
|
50
|
+
"Prefer a matching project specialist. Otherwise use research for reading/evidence or focused review questions, implement for code/docs/tests/UI changes, verify for running checks, ui-qa for real user-interface testing across browsers, TUIs, and desktop GUIs, and frontier-review for an independent post-implementation code review when that type is allowed. Oracle is a deliberate strong second opinion, not routine code review or the default for difficult work.",
|
|
49
51
|
"Return only strict JSON with this shape: {\"routes\":[{\"id\":\"task-id\",\"subagentType\":\"type\"}]}",
|
|
50
52
|
"Do not include markdown, comments, explanations, or unknown types.",
|
|
51
53
|
].join("\n");
|
|
@@ -58,14 +60,28 @@ export async function routeSubagentTasks(
|
|
|
58
60
|
): Promise<RoutedSubagentTasks> {
|
|
59
61
|
if (signal?.aborted) throw new Error("Aborted");
|
|
60
62
|
// Validate even the fast path: an explicit typo must not bypass profiles.
|
|
61
|
-
tasks = tasks.map((task) =>
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
tasks = tasks.map((task) => {
|
|
64
|
+
if (!hasText(task.subagentType)) return task;
|
|
65
|
+
const subagentType = normalizeSubagentType(task.subagentType, config);
|
|
66
|
+
return subagentType !== task.subagentType ? { ...task, subagentType } : task;
|
|
67
|
+
});
|
|
68
|
+
const parentModel = currentModelRef(ctx.model);
|
|
69
|
+
const effectiveConfig = filterSubagentConfigForParentModel(config, parentModel);
|
|
64
70
|
const invalidTasks = tasks.filter((task) => hasText(task.subagentType)
|
|
65
71
|
&& !Object.prototype.hasOwnProperty.call(config.types, task.subagentType));
|
|
66
72
|
if (invalidTasks.length > 0) {
|
|
67
|
-
throw routingError(`Unknown subagentType: ${invalidTasks.map((task) => `${task.id}=${JSON.stringify(task.subagentType)}`).join(", ")}.`, invalidTasks,
|
|
73
|
+
throw routingError(`Unknown subagentType: ${invalidTasks.map((task) => `${task.id}=${JSON.stringify(task.subagentType)}`).join(", ")}.`, invalidTasks, effectiveConfig);
|
|
74
|
+
}
|
|
75
|
+
const unavailableTasks = tasks.filter((task) => hasText(task.subagentType)
|
|
76
|
+
&& !Object.prototype.hasOwnProperty.call(effectiveConfig.types, task.subagentType));
|
|
77
|
+
if (unavailableTasks.length > 0) {
|
|
78
|
+
throw routingError(
|
|
79
|
+
`subagentType unavailable for parent model ${parentModel ?? "(unknown)"}: ${unavailableTasks.map((task) => `${task.id}=${JSON.stringify(task.subagentType)}`).join(", ")}.`,
|
|
80
|
+
unavailableTasks,
|
|
81
|
+
effectiveConfig,
|
|
82
|
+
);
|
|
68
83
|
}
|
|
84
|
+
config = effectiveConfig;
|
|
69
85
|
const autoTasks = tasks.filter((task) => !hasText(task.subagentType));
|
|
70
86
|
if (autoTasks.length === 0) return { tasks, usedLlm: false, routes: {}, warnings: [] };
|
|
71
87
|
|
|
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { selectSuitableToolsForModel } from "../../lib/tool-args.js";
|
|
6
|
-
import { BROWSER_QA_RUNNER_ENV, getBrowserQaRunnerPath } from "./browser-qa.js";
|
|
6
|
+
import { BROWSER_QA_RUNNER_ENV, getBrowserQaRunnerPath, getUiQaRunnerPath, isUiQaType, UI_QA_RUNNER_ENV } from "./browser-qa.js";
|
|
7
7
|
import { validateBasename } from "./paths.js";
|
|
8
8
|
import { getPiInvocation } from "./pi-invocation.js";
|
|
9
9
|
import { writePromptFile } from "./prompt.js";
|
|
@@ -20,12 +20,11 @@ export interface SpawnAgentOptions {
|
|
|
20
20
|
parentSession?: string;
|
|
21
21
|
timeoutMs?: number;
|
|
22
22
|
maxResultBytes?: number;
|
|
23
|
-
/** Explicit skills to load after disabling normal skill discovery. */
|
|
24
|
-
isolatedSkills?: string[];
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
export const DEFAULT_AGENT_TIMEOUT_MS = 30 * 60 * 1000;
|
|
28
26
|
const BROWSER_QA_WORKSPACE_DIR = "browser-qa";
|
|
27
|
+
const UI_QA_WORKSPACE_DIR = "ui-qa";
|
|
29
28
|
const SUBAGENT_AGENT_DIR_ENV = "PI_SUBAGENT_AGENT_DIR";
|
|
30
29
|
const AGENT_TIMEOUT_EXIT_CODE = 124;
|
|
31
30
|
const AGENT_TIMEOUT_KILL_GRACE_MS = 5_000;
|
|
@@ -50,7 +49,7 @@ export function spawnAgent(
|
|
|
50
49
|
validateBasename(task.id, "task.id");
|
|
51
50
|
const agentDir = path.join(runDir, task.id);
|
|
52
51
|
fs.mkdirSync(agentDir, { recursive: true });
|
|
53
|
-
|
|
52
|
+
prepareUiQaWorkspace(agentDir, task.subagentType);
|
|
54
53
|
|
|
55
54
|
// Clean previous state when reusing a run directory/agent id.
|
|
56
55
|
for (const f of [
|
|
@@ -89,10 +88,9 @@ export function spawnAgent(
|
|
|
89
88
|
// detached subprocesses do not depend on persisted local pi settings.
|
|
90
89
|
const persistSessions = shouldPersistSubagentSessions();
|
|
91
90
|
const sessionDir = persistSessions ? getAgentSessionDir(agentDir) : undefined;
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
const
|
|
95
|
-
const forwardedExtraArgs = isolateSkills ? withoutSkillArgs(extraArgs) : extraArgs;
|
|
91
|
+
// Sub-agent roles are self-contained. Never inherit/discover skills, and do
|
|
92
|
+
// not allow profile/user extra args to re-enable or inject them.
|
|
93
|
+
const forwardedExtraArgs = withoutSkillArgs(extraArgs);
|
|
96
94
|
if (sessionDir) fs.mkdirSync(sessionDir, { recursive: true });
|
|
97
95
|
const piArgs: string[] = ["--mode", "rpc"];
|
|
98
96
|
if (sessionDir) piArgs.push("--session-dir", sessionDir);
|
|
@@ -101,16 +99,15 @@ export function spawnAgent(
|
|
|
101
99
|
piArgs.push("--extension", getModelToolsExtensionPath());
|
|
102
100
|
// Preserve `--no-extensions` unless this invocation explicitly selects an
|
|
103
101
|
// Antigravity model. Environment/default models do not opt the provider in.
|
|
104
|
-
|
|
102
|
+
const configuredModel = task.model || getEnvModel();
|
|
103
|
+
const selectedModel = resolveSelectedModel(configuredModel, forwardedExtraArgs);
|
|
104
|
+
const explicitModel = resolveSelectedModel(task.model, forwardedExtraArgs);
|
|
105
|
+
if (explicitModel?.startsWith("antigravity/") && explicitModel.length > "antigravity/".length) {
|
|
105
106
|
piArgs.push("--extension", getAntigravityAuthExtensionPath());
|
|
106
107
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
const envModel = task.model || getEnvModel();
|
|
112
|
-
if (envModel) piArgs.push("--model", envModel);
|
|
113
|
-
const selectedTools = task.tools ? filterSubagentTools(selectSuitableToolsForModel(envModel, task.tools)) : undefined;
|
|
108
|
+
piArgs.push("--no-skills");
|
|
109
|
+
if (configuredModel) piArgs.push("--model", configuredModel);
|
|
110
|
+
const selectedTools = task.tools ? filterSubagentTools(selectSuitableToolsForModel(selectedModel, task.tools)) : undefined;
|
|
114
111
|
if (selectedTools) {
|
|
115
112
|
if (selectedTools.length > 0) piArgs.push("--tools", selectedTools.join(","));
|
|
116
113
|
else piArgs.push("--no-tools");
|
|
@@ -119,6 +116,10 @@ export function spawnAgent(
|
|
|
119
116
|
|
|
120
117
|
// User-supplied extra args (e.g. --thinking high)
|
|
121
118
|
piArgs.push(...forwardedExtraArgs);
|
|
119
|
+
// The child always runs one explicitly selected model. Override persisted
|
|
120
|
+
// enabledModels (and any extra --models value) so isolated children do not
|
|
121
|
+
// resolve unrelated provider patterns before their limited extensions load.
|
|
122
|
+
if (selectedModel) piArgs.push("--models", selectedModel);
|
|
122
123
|
// Keep recursive/interactive parent-only tools disabled even if explicit
|
|
123
124
|
// sub-agent extraArgs load additional extensions or override --tools.
|
|
124
125
|
piArgs.push("--extension", getSubagentToolGuardExtensionPath());
|
|
@@ -151,7 +152,7 @@ export function spawnAgent(
|
|
|
151
152
|
|
|
152
153
|
const proc = spawn(invocation.command, invocation.args, {
|
|
153
154
|
cwd,
|
|
154
|
-
env: subagentEnvironment(process.env, task.subagentType
|
|
155
|
+
env: subagentEnvironment(process.env, isUiQaType(task.subagentType) ? agentDir : undefined),
|
|
155
156
|
stdio: ["pipe", "pipe", "pipe"],
|
|
156
157
|
detached: process.platform !== "win32",
|
|
157
158
|
});
|
|
@@ -477,8 +478,8 @@ function withoutSkillArgs(args: string[]): string[] {
|
|
|
477
478
|
return filtered;
|
|
478
479
|
}
|
|
479
480
|
|
|
480
|
-
function
|
|
481
|
-
let model =
|
|
481
|
+
function resolveSelectedModel(configuredModel: string | undefined, extraArgs: string[]): string | undefined {
|
|
482
|
+
let model = configuredModel?.trim() || undefined;
|
|
482
483
|
for (let index = 0; index < extraArgs.length; index += 1) {
|
|
483
484
|
const arg = extraArgs[index];
|
|
484
485
|
if (arg === "--model" || arg === "-m") {
|
|
@@ -490,7 +491,7 @@ function usesAntigravityModel(taskModel: string | undefined, extraArgs: string[]
|
|
|
490
491
|
model = arg.slice("--model=".length).trim() || undefined;
|
|
491
492
|
}
|
|
492
493
|
}
|
|
493
|
-
return
|
|
494
|
+
return model;
|
|
494
495
|
}
|
|
495
496
|
|
|
496
497
|
function getModelToolsExtensionPath(): string {
|
|
@@ -696,10 +697,14 @@ function compactRpcEventForProgress(event: RpcEventRecord): Record<string, unkno
|
|
|
696
697
|
toolName: typeof event.toolName === "string" ? event.toolName : undefined,
|
|
697
698
|
});
|
|
698
699
|
}
|
|
699
|
-
if (event.type === "message_end") {
|
|
700
|
+
if (event.type === "message_start" || event.type === "message_end") {
|
|
700
701
|
return stripUndefined({
|
|
701
702
|
type: event.type,
|
|
702
|
-
role:
|
|
703
|
+
role: typeof event.role === "string"
|
|
704
|
+
? event.role
|
|
705
|
+
: isRecord(event.message) && typeof event.message.role === "string"
|
|
706
|
+
? event.message.role
|
|
707
|
+
: undefined,
|
|
703
708
|
stopReason: isRecord(event.message) && typeof event.message.stopReason === "string" ? event.message.stopReason : undefined,
|
|
704
709
|
});
|
|
705
710
|
}
|
|
@@ -817,21 +822,30 @@ function subagentEnvironment(env: NodeJS.ProcessEnv, agentDir?: string): NodeJS.
|
|
|
817
822
|
};
|
|
818
823
|
delete result[SUBAGENT_AGENT_DIR_ENV];
|
|
819
824
|
delete result[BROWSER_QA_RUNNER_ENV];
|
|
825
|
+
delete result[UI_QA_RUNNER_ENV];
|
|
820
826
|
if (agentDir) {
|
|
821
827
|
result[SUBAGENT_AGENT_DIR_ENV] = fs.realpathSync(agentDir);
|
|
822
828
|
result[BROWSER_QA_RUNNER_ENV] = getBrowserQaRunnerPath();
|
|
829
|
+
result[UI_QA_RUNNER_ENV] = getUiQaRunnerPath();
|
|
823
830
|
}
|
|
824
831
|
return result;
|
|
825
832
|
}
|
|
826
833
|
|
|
827
|
-
function
|
|
828
|
-
const workspace = path.join(agentDir,
|
|
834
|
+
function prepareUiQaWorkspace(agentDir: string, subagentType: string | undefined): void {
|
|
835
|
+
const workspace = path.join(agentDir, UI_QA_WORKSPACE_DIR);
|
|
836
|
+
const browserWorkspace = path.join(agentDir, BROWSER_QA_WORKSPACE_DIR);
|
|
829
837
|
fs.rmSync(workspace, { recursive: true, force: true });
|
|
830
|
-
|
|
831
|
-
|
|
838
|
+
fs.rmSync(browserWorkspace, { recursive: true, force: true });
|
|
839
|
+
if (!isUiQaType(subagentType)) return;
|
|
840
|
+
const flows = path.join(browserWorkspace, "flows");
|
|
841
|
+
const uiFlows = path.join(workspace, "flows");
|
|
842
|
+
fs.mkdirSync(workspace, { recursive: true, mode: 0o700 });
|
|
843
|
+
fs.mkdirSync(uiFlows, { recursive: true, mode: 0o700 });
|
|
832
844
|
fs.mkdirSync(flows, { recursive: true, mode: 0o700 });
|
|
833
845
|
if (process.platform !== "win32") {
|
|
834
846
|
fs.chmodSync(workspace, 0o700);
|
|
847
|
+
fs.chmodSync(uiFlows, 0o700);
|
|
848
|
+
fs.chmodSync(browserWorkspace, 0o700);
|
|
835
849
|
fs.chmodSync(flows, 0o700);
|
|
836
850
|
}
|
|
837
851
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { activityFromProgressRecord } from "./activity.js";
|
|
3
4
|
import { hasLaunchedAgentPrompt, isDir } from "./paths.js";
|
|
4
5
|
import { readStructuredResult } from "./structured-result.js";
|
|
5
6
|
import type { AgentResult, AgentState, RpcEventRecord, RunState } from "./types.js";
|
|
6
7
|
|
|
7
8
|
const MAX_RPC_EVENT_LINE_CHARS = 1024 * 1024;
|
|
9
|
+
const LAST_ACTIVITY_TAIL_BYTES = 64 * 1024;
|
|
8
10
|
|
|
9
11
|
interface AgentStateReadOptions {
|
|
10
12
|
includeLineCounts?: boolean;
|
|
@@ -67,6 +69,9 @@ export function getAgentState(
|
|
|
67
69
|
state.finishedAt = fs.readFileSync(finishedAtFile, "utf-8").trim();
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
const lastActivity = readLastActivity(path.join(agentDir, "progress.jsonl"));
|
|
73
|
+
if (lastActivity) state.lastActivity = lastActivity;
|
|
74
|
+
|
|
70
75
|
const retryPendingFile = path.join(agentDir, "retry_pending");
|
|
71
76
|
const stopRequestedFile = path.join(agentDir, "stop_requested");
|
|
72
77
|
if (fs.existsSync(retryPendingFile) && !fs.existsSync(stopRequestedFile) && state.status !== "running" && state.status !== "done") {
|
|
@@ -109,6 +114,41 @@ function readPid(pidFile: string): number | undefined {
|
|
|
109
114
|
return isNaN(pid) ? undefined : pid;
|
|
110
115
|
}
|
|
111
116
|
|
|
117
|
+
function readLastActivity(filePath: string): AgentState["lastActivity"] | undefined {
|
|
118
|
+
if (!fs.existsSync(filePath)) return undefined;
|
|
119
|
+
let fd: number | undefined;
|
|
120
|
+
try {
|
|
121
|
+
fd = fs.openSync(filePath, "r");
|
|
122
|
+
const size = fs.fstatSync(fd).size;
|
|
123
|
+
if (size <= 0) return undefined;
|
|
124
|
+
const start = Math.max(0, size - LAST_ACTIVITY_TAIL_BYTES);
|
|
125
|
+
const buffer = Buffer.allocUnsafe(size - start);
|
|
126
|
+
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, start);
|
|
127
|
+
if (bytesRead <= 0) return undefined;
|
|
128
|
+
const lines = buffer.toString("utf-8", 0, bytesRead).split(/\r?\n/);
|
|
129
|
+
if (start > 0) {
|
|
130
|
+
const previousByte = Buffer.allocUnsafe(1);
|
|
131
|
+
const previousBytesRead = fs.readSync(fd, previousByte, 0, 1, start - 1);
|
|
132
|
+
if (previousBytesRead === 1 && previousByte[0] !== 0x0a) lines.shift();
|
|
133
|
+
}
|
|
134
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
135
|
+
const line = lines[index]?.trim();
|
|
136
|
+
if (!line) continue;
|
|
137
|
+
try {
|
|
138
|
+
const activity = activityFromProgressRecord(JSON.parse(line) as unknown);
|
|
139
|
+
if (activity) return activity;
|
|
140
|
+
} catch {
|
|
141
|
+
// Ignore malformed or partially written tail lines and keep scanning.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
return undefined;
|
|
146
|
+
} finally {
|
|
147
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
112
152
|
function hasRpcPromptFailure(eventsFile: string): boolean {
|
|
113
153
|
if (!fs.existsSync(eventsFile)) return false;
|
|
114
154
|
let fd: number | undefined;
|
|
@@ -39,6 +39,13 @@ export interface AgentState {
|
|
|
39
39
|
eventLines?: number;
|
|
40
40
|
/** How many times this agent has been retried (0 = first attempt). */
|
|
41
41
|
retryCount?: number;
|
|
42
|
+
/** Most recent user-visible activity observed from the sub-agent RPC stream. */
|
|
43
|
+
lastActivity?: AgentActivity;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AgentActivity {
|
|
47
|
+
label: string;
|
|
48
|
+
at: string;
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
/** Retry configuration for failed agents. */
|
|
@@ -59,40 +59,58 @@ export async function decideUltraworkAuto(
|
|
|
59
59
|
if (!routing.enabled) return "none";
|
|
60
60
|
if (signal?.aborted) throw new Error("Aborted");
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
62
|
+
const refs = [routing.model, ...routing.fallbackModels];
|
|
63
|
+
const parentModel = currentModelRef(ctx.model);
|
|
64
|
+
if (parentModel) refs.push(parentModel);
|
|
65
|
+
const seen = new Set<string>();
|
|
66
|
+
|
|
67
|
+
for (const ref of refs) {
|
|
68
|
+
const modelRef = ref?.trim();
|
|
69
|
+
if (!modelRef || seen.has(modelRef)) continue;
|
|
70
|
+
seen.add(modelRef);
|
|
71
|
+
|
|
72
|
+
let resolved: Awaited<ReturnType<typeof resolveModelRef>>;
|
|
73
|
+
try {
|
|
74
|
+
resolved = await resolveModelRef(ctx, modelRef);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (signal?.aborted || isAbortError(error)) throw error;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (!resolved) continue;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const response = await completeWithModelRegistry(
|
|
83
|
+
ctx.modelRegistry,
|
|
84
|
+
resolved.model,
|
|
85
|
+
{
|
|
86
|
+
systemPrompt: CLASSIFIER_SYSTEM_PROMPT,
|
|
87
|
+
messages: [
|
|
88
|
+
{
|
|
89
|
+
role: "user" as const,
|
|
90
|
+
content: [{ type: "text" as const, text: buildClassifierPrompt(userText) }],
|
|
91
|
+
timestamp: Date.now(),
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
apiKey: resolved.apiKey,
|
|
97
|
+
headers: resolved.headers,
|
|
98
|
+
env: resolved.env,
|
|
99
|
+
cacheRetention: "none",
|
|
100
|
+
maxRetries: routing.maxRetries,
|
|
101
|
+
maxTokens: Math.min(routing.maxTokens, 32),
|
|
102
|
+
signal,
|
|
103
|
+
timeoutMs: routing.timeoutMs,
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
if (signal?.aborted || response.stopReason === "aborted") throw new Error("Aborted");
|
|
107
|
+
if (response.stopReason === "error") continue;
|
|
108
|
+
return parseUltraworkAutoDecision(responseText(response));
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (signal?.aborted || isAbortError(error)) throw error;
|
|
111
|
+
}
|
|
95
112
|
}
|
|
113
|
+
return "none";
|
|
96
114
|
}
|
|
97
115
|
|
|
98
116
|
export function parseUltraworkAutoDecision(raw: string): UltraworkAutoDecision {
|
|
@@ -128,18 +146,6 @@ function buildClassifierPrompt(userText: string): string {
|
|
|
128
146
|
].join("\n");
|
|
129
147
|
}
|
|
130
148
|
|
|
131
|
-
async function resolveClassifierModel(ctx: UltraworkAutoContext, modelRef: string): Promise<{
|
|
132
|
-
model: Model<Api>;
|
|
133
|
-
apiKey?: string;
|
|
134
|
-
headers?: ProviderHeaders;
|
|
135
|
-
env?: Record<string, string>;
|
|
136
|
-
} | undefined> {
|
|
137
|
-
const configured = await resolveModelRef(ctx, modelRef);
|
|
138
|
-
if (configured) return configured;
|
|
139
|
-
const parentModel = currentModelRef(ctx.model);
|
|
140
|
-
return parentModel && parentModel !== modelRef ? resolveModelRef(ctx, parentModel) : undefined;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
149
|
async function resolveModelRef(ctx: UltraworkAutoContext, modelRef: string): Promise<{
|
|
144
150
|
model: Model<Api>;
|
|
145
151
|
apiKey?: string;
|