@rexvane/piweb 0.3.14 → 0.3.16
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +12 -9
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +6 -6
- package/.next/react-loadable-manifest.json +16 -4
- package/.next/required-server-files.js +3 -1
- package/.next/required-server-files.json +3 -1
- package/.next/routes-manifest.json +20 -0
- package/.next/server/app/_global-error/page.js +2 -2
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +7 -4
- package/.next/server/app/_not-found.segments/_full.segment.rsc +7 -4
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +5 -2
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +5 -2
- package/.next/server/app/api/agent/[id]/events/route.js +2 -2
- package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/[id]/route.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
- package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/new/route.js +1 -1
- package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
- package/.next/server/app/api/config/route.js +1 -1
- package/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/server/app/api/files/route.js +1 -1
- package/.next/server/app/api/files/route.js.nft.json +1 -1
- package/.next/server/app/api/git/route.js +2 -2
- package/.next/server/app/api/git/route.js.nft.json +1 -1
- package/.next/server/app/api/growth/route.js +1 -1
- package/.next/server/app/api/growth/route.js.nft.json +1 -1
- package/.next/server/app/api/health/route.js +1 -1
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/models/route.js.nft.json +1 -1
- package/.next/server/app/api/pi-settings/route.js +1 -1
- package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
- package/.next/server/app/api/plugins/route.js +1 -1
- package/.next/server/app/api/plugins/route.js.nft.json +1 -1
- package/.next/server/app/api/prompts/route.js +1 -1
- package/.next/server/app/api/prompts/route.js.nft.json +1 -1
- package/.next/server/app/api/security/route.js +1 -1
- package/.next/server/app/api/security/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/import/route.js +3 -3
- package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/route.js +2 -2
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/swarm/[id]/route.js +1 -0
- package/.next/server/app/api/swarm/[id]/route.js.nft.json +1 -0
- package/.next/server/app/api/swarm/[id]/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/swarm/route.js +1 -0
- package/.next/server/app/api/swarm/route.js.nft.json +1 -0
- package/.next/server/app/api/swarm/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/tests/route.js +2 -0
- package/.next/server/app/api/tests/route.js.nft.json +1 -0
- package/.next/server/app/api/tests/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/update/route.js +2 -2
- package/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/server/app/api/version/route.js +1 -1
- package/.next/server/app/api/version/route.js.nft.json +1 -1
- package/.next/server/app/api/web-auth/route.js +1 -1
- package/.next/server/app/api/workspaces/route.js +1 -1
- package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
- package/.next/server/app/icon.svg/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +9 -6
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +7 -4
- package/.next/server/app/index.segments/_full.segment.rsc +9 -6
- package/.next/server/app/index.segments/_tree.segment.rsc +5 -2
- package/.next/server/app/login/page.js +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +1 -1
- package/.next/server/app/login.rsc +8 -5
- package/.next/server/app/login.segments/_full.segment.rsc +8 -5
- package/.next/server/app/login.segments/_tree.segment.rsc +5 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +6 -3
- package/.next/server/app/page.js +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +12 -9
- package/.next/server/chunks/496.js +1 -1
- package/.next/server/chunks/532.js +1 -1
- package/.next/server/chunks/686.js +6 -2
- package/.next/server/chunks/757.js +1 -0
- package/.next/server/chunks/775.js +3 -0
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/0t1jBUAuVaTFTXxRyIo3q/_buildManifest.js +1 -0
- package/.next/static/chunks/2608.43c58236c9cce8e6.js +1 -0
- package/.next/static/chunks/{2716.a9e808190a4e2424.js → 2716.4ec42df1d51354c9.js} +1 -1
- package/.next/static/chunks/4632.62139b565b657aac.js +1 -0
- package/.next/static/chunks/{4673.d3d0a7f2599080fb.js → 4673.7796104395deb8d7.js} +1 -1
- package/.next/static/chunks/4686-17f21a006739a596.js +1 -0
- package/.next/static/chunks/7577.10af9bb661652da8.js +1 -0
- package/.next/static/chunks/8866.38ec4ac4ee5a1b74.js +1 -0
- package/.next/static/chunks/app/_global-error/page-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/events/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/tree/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/agent/new/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/config/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/files/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/git/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/growth/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/health/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/models/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/pi-settings/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/plugins/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/prompts/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/security/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/export/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/sessions/import/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/skills/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/swarm/[id]/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/swarm/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/tests/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/update/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/version/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/web-auth/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/workspaces/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/page-14379b8060d5cf07.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/webpack-d1a69ad9edb38b11.js +1 -0
- package/.next/static/css/d0a0162cb481abe8.css +1 -0
- package/.next/types/app/api/swarm/[id]/route.ts +353 -0
- package/.next/types/app/api/swarm/route.ts +353 -0
- package/.next/types/app/api/tests/route.ts +353 -0
- package/.next/types/routes.d.ts +4 -1
- package/.next/types/validator.ts +27 -0
- package/README.md +61 -7
- package/assets/archive.png +0 -0
- package/assets/growth-1.png +0 -0
- package/assets/growth-2.png +0 -0
- package/assets/import.png +0 -0
- package/assets/main.png +0 -0
- package/assets/outline.png +0 -0
- package/assets/prompts-1.png +0 -0
- package/assets/prompts-2.png +0 -0
- package/assets/providers-1.png +0 -0
- package/assets/providers-2.png +0 -0
- package/assets/providers-3.png +0 -0
- package/assets/trace-1.png +0 -0
- package/assets/trace-2.png +0 -0
- package/assets/trace-3.png +0 -0
- package/assets/trace-4.png +0 -0
- package/assets/viewer-1.png +0 -0
- package/assets/viewer-2.png +0 -0
- package/next.config.ts +6 -2
- package/package.json +10 -5
- package/scripts/launcher.mjs +1 -1
- package/scripts/managed-niubash-assets.mjs +146 -0
- package/scripts/pick-folder.ps1 +11 -2
- package/scripts/prepare-niubash-packages.mjs +182 -0
- package/scripts/release.mjs +11 -31
- package/scripts/run-test-file.mjs +34 -0
- package/scripts/smoke-managed-niubash.mjs +62 -0
- package/scripts/verify-package.mjs +53 -0
- package/scripts/verify-release-config.mjs +24 -0
- package/src/app/api/config/route.ts +4 -1
- package/src/app/api/security/route.ts +8 -3
- package/src/app/api/swarm/[id]/route.ts +28 -0
- package/src/app/api/swarm/route.ts +24 -0
- package/src/app/api/tests/route.ts +27 -0
- package/src/app/api/update/route.ts +8 -2
- package/src/app/globals.css +777 -0
- package/src/app/layout.tsx +13 -6
- package/src/components/AppShell.tsx +505 -199
- package/src/components/ChatInput.tsx +153 -26
- package/src/components/ChatWindow.tsx +46 -40
- package/src/components/ContextMeter.tsx +11 -17
- package/src/components/FileViewer.tsx +1 -1
- package/src/components/GitPanel.tsx +13 -11
- package/src/components/ModelSelector.tsx +19 -6
- package/src/components/PiMark.tsx +5 -38
- package/src/components/ProjectPanel.tsx +1 -0
- package/src/components/PromptPanel.tsx +132 -51
- package/src/components/SessionImportSection.tsx +53 -37
- package/src/components/SessionSidebar.tsx +159 -57
- package/src/components/SettingsPanel.tsx +330 -193
- package/src/components/SwarmCoordinatorPod.tsx +1021 -0
- package/src/components/TestInspectorPod.tsx +549 -0
- package/src/components/icons.tsx +23 -0
- package/src/hooks/usePiWeb.ts +112 -8
- package/src/i18n.tsx +59 -3
- package/src/lib/agent-manager.ts +182 -11
- package/src/lib/command-validation.ts +9 -1
- package/src/lib/delegation.ts +93 -0
- package/src/lib/managed-niubash.ts +231 -0
- package/src/lib/pi.ts +3 -2
- package/src/lib/runtime-activity.ts +27 -0
- package/src/lib/session-import/claude.ts +13 -2
- package/src/lib/session-import/codex.ts +44 -8
- package/src/lib/session-import/files.ts +14 -0
- package/src/lib/session-import/grok.ts +54 -24
- package/src/lib/session-import/index.ts +5 -0
- package/src/lib/session-import/lock.ts +25 -0
- package/src/lib/session-import/types.ts +12 -0
- package/src/lib/session-import/writer.ts +8 -3
- package/src/lib/skills-service.ts +16 -9
- package/src/lib/swarm-service.ts +371 -0
- package/src/lib/test-run-service.ts +122 -0
- package/src/lib/theme.ts +68 -1
- package/src/lib/trajectory.ts +1 -0
- package/src/lib/types.ts +14 -0
- package/src/lib/workflow.ts +55 -0
- package/.next/static/LOFl9bqqMV2goBV8erESO/_buildManifest.js +0 -1
- package/.next/static/chunks/2608.ec8ff1f5767c9056.js +0 -1
- package/.next/static/chunks/4632.112d2e893d48d188.js +0 -1
- package/.next/static/chunks/4686-7d694def920ff093.js +0 -1
- package/.next/static/chunks/app/_global-error/page-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/tree/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/config/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/files/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/git/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/growth/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/health/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/models/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/pi-settings/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/prompts/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/security/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/sessions/import/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/skills/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/update/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/version/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/web-auth/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/workspaces/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/page-9a5440047c5a415c.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/webpack-d28e132e1b8c100c.js +0 -1
- package/.next/static/css/b2d35abc828d4713.css +0 -1
- package/assets/showcase.png +0 -0
- package/bin/pi.js +0 -50
- /package/.next/static/{LOFl9bqqMV2goBV8erESO → 0t1jBUAuVaTFTXxRyIo3q}/_ssgManifest.js +0 -0
package/src/lib/agent-manager.ts
CHANGED
|
@@ -33,8 +33,12 @@ import { TrajLedger, buildTrajectoryFromEntries, toTrajTokens } from "./trajecto
|
|
|
33
33
|
import { startArchivedSessionRetention } from "./session-retention";
|
|
34
34
|
import { sanitizeToolOutput } from "./text-sanitize";
|
|
35
35
|
import { BoundaryError } from "./path-security";
|
|
36
|
+
import { beginActivity } from "./runtime-activity";
|
|
36
37
|
import { estimateTokensOf } from "./process-format";
|
|
37
38
|
import { getPiSettings } from "./pi-settings";
|
|
39
|
+
import { createManagedNiubashTools } from "./managed-niubash";
|
|
40
|
+
import { createDelegationTool } from "./delegation";
|
|
41
|
+
import { awaitingGoalMessage, isWorkflowMode, planCanRun, readWorkflow, saveWorkflow, workflowInstruction, type WorkflowState } from "./workflow";
|
|
38
42
|
import { clampThinkingLevel, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
39
43
|
import type {
|
|
40
44
|
AgentCommand,
|
|
@@ -65,6 +69,7 @@ interface Managed {
|
|
|
65
69
|
ledger: TrajLedger;
|
|
66
70
|
lastActive: number;
|
|
67
71
|
toolPreset: ToolPreset;
|
|
72
|
+
workflow: WorkflowState;
|
|
68
73
|
/** 显式逐项选择优先于 preset;保留失效名称以便资源重新启用后恢复用户选择。 */
|
|
69
74
|
customActiveTools?: string[];
|
|
70
75
|
resourceVersion: number;
|
|
@@ -526,6 +531,20 @@ function translate(m: Managed, evt: AgentSessionEvent): void {
|
|
|
526
531
|
break;
|
|
527
532
|
case "agent_settled":
|
|
528
533
|
m.runActive = false;
|
|
534
|
+
if (m.workflow.mode === "plan") {
|
|
535
|
+
if (m.workflow.planStatus === "planning") {
|
|
536
|
+
const answer = [...(m.session?.messages ?? [])].reverse().find((message) => message.role === "assistant");
|
|
537
|
+
const ready = answer?.role === "assistant" && answer.stopReason === "stop" && answer.content.some((part) => part.type === "text" && part.text.trim());
|
|
538
|
+
const entry = ready ? m.sm.getBranch().slice().reverse().find((item) => item.type === "message" && item.message.role === "assistant" && item.message.stopReason === "stop") : undefined;
|
|
539
|
+
setWorkflow(m, { ...m.workflow, planStatus: entry ? "ready" : "idle", planId: entry?.id });
|
|
540
|
+
} else if (m.workflow.planStatus === "executing") {
|
|
541
|
+
// 执行报错或被中止时方案仍待执行:以当前叶节点为新的批准锚点,可以直接再次执行。
|
|
542
|
+
const answer = [...(m.session?.messages ?? [])].reverse().find((message) => message.role === "assistant");
|
|
543
|
+
const failed = answer?.role === "assistant" && (answer.stopReason === "error" || answer.stopReason === "aborted");
|
|
544
|
+
const anchor = failed ? m.sm.getLeafId() : null;
|
|
545
|
+
setWorkflow(m, anchor ? { ...m.workflow, planStatus: "ready", planId: anchor } : { ...m.workflow, planStatus: "idle", planId: undefined });
|
|
546
|
+
}
|
|
547
|
+
}
|
|
529
548
|
publish(m, { type: "status", isStreaming: false, state: "idle", ts: now });
|
|
530
549
|
publishUsage(m);
|
|
531
550
|
if (m.resourceReloadPending) queueMicrotask(() => void reloadManagedResources(m));
|
|
@@ -635,6 +654,7 @@ function registerManaged(sessionPath: string, cwd: string, sm: SessionManager):
|
|
|
635
654
|
ledger: new TrajLedger(initialTrajectory),
|
|
636
655
|
lastActive: Date.now(),
|
|
637
656
|
toolPreset: "readonly",
|
|
657
|
+
workflow: readWorkflow(sm),
|
|
638
658
|
resourceVersion: 0,
|
|
639
659
|
resourceReloadPending: false,
|
|
640
660
|
resourceReloading: false,
|
|
@@ -783,12 +803,33 @@ function publishEnvironmentTrajectory(m: Managed, session: AgentSession): void {
|
|
|
783
803
|
}
|
|
784
804
|
|
|
785
805
|
export async function ensureSession(m: Managed): Promise<AgentSession> {
|
|
806
|
+
const release = beginActivity("session");
|
|
807
|
+
try { return await ensureSessionActive(m); }
|
|
808
|
+
finally { release(); }
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async function ensureSessionActive(m: Managed): Promise<AgentSession> {
|
|
786
812
|
if (m.disposed) throw new Error("session is disposed");
|
|
787
813
|
if (m.creating) return m.creating;
|
|
788
814
|
if (m.session) return m.session;
|
|
789
815
|
m.creating = (async () => {
|
|
790
816
|
await resourceLoaderReady(m.cwd);
|
|
791
|
-
const loader = await createSessionResourceLoader(m.cwd
|
|
817
|
+
const loader = await createSessionResourceLoader(m.cwd, [{
|
|
818
|
+
name: "piweb-workflow",
|
|
819
|
+
hidden: true,
|
|
820
|
+
factory: (pi) => {
|
|
821
|
+
pi.on("before_agent_start", (event) => {
|
|
822
|
+
const instruction = workflowInstruction(m.workflow);
|
|
823
|
+
if (instruction) {
|
|
824
|
+
if (event.systemPromptOptions.forceSystemPrompt !== undefined) event.systemPromptOptions.forceSystemPrompt += `\n\n${instruction}`;
|
|
825
|
+
else event.systemPromptOptions.sections.piweb_workflow = instruction;
|
|
826
|
+
} else delete event.systemPromptOptions.sections.piweb_workflow;
|
|
827
|
+
if (m.workflow.mode === "plan" && m.workflow.planStatus !== "executing") {
|
|
828
|
+
event.systemPromptOptions.selectedTools = event.systemPromptOptions.selectedTools.filter((name) => [...TOOL_PRESETS.readonly, "piweb_delegate"].includes(name));
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
},
|
|
832
|
+
}]);
|
|
792
833
|
let created: AgentSession | undefined;
|
|
793
834
|
try {
|
|
794
835
|
if (m.disposed) throw new Error("session is disposed");
|
|
@@ -799,6 +840,8 @@ export async function ensureSession(m: Managed): Promise<AgentSession> {
|
|
|
799
840
|
modelRuntime: await getModelRuntime(),
|
|
800
841
|
resourceLoader: loader,
|
|
801
842
|
settingsManager: getSettingsManager(m.cwd),
|
|
843
|
+
// Windows: 同名 bash 覆盖 SDK 内置工具,指向托管 niubash;其它平台返回 [],走 Pi 默认。
|
|
844
|
+
customTools: [...createManagedNiubashTools(m.cwd), createDelegationTool(m.cwd, () => m.session)],
|
|
802
845
|
});
|
|
803
846
|
created = session;
|
|
804
847
|
if (m.disposed) throw new Error("session is disposed");
|
|
@@ -845,7 +888,9 @@ function isManagedBusy(m: Managed): boolean {
|
|
|
845
888
|
|
|
846
889
|
function allowedToolNames(m: Managed, session: AgentSession): string[] {
|
|
847
890
|
const allowed = m.customActiveTools ?? (m.toolPreset === "full" ? undefined : TOOL_PRESETS[m.toolPreset]);
|
|
848
|
-
|
|
891
|
+
const effective = allowed ? [...allowed, ...(m.customActiveTools || m.toolPreset === "readonly" ? [] : ["piweb_delegate"])] : undefined;
|
|
892
|
+
const planning = m.workflow.mode === "plan" && m.workflow.planStatus !== "executing";
|
|
893
|
+
return listAllToolNames(session).filter((name) => (!effective || effective.includes(name)) && (!planning || [...TOOL_PRESETS.readonly, "piweb_delegate"].includes(name)));
|
|
849
894
|
}
|
|
850
895
|
|
|
851
896
|
function applyToolPolicy(m: Managed, session: AgentSession): void {
|
|
@@ -856,6 +901,20 @@ function publishTools(m: Managed, session: AgentSession): void {
|
|
|
856
901
|
publish(m, { type: "tools", active: listActiveTools(session), all: listAllToolNames(session), toolPreset: m.toolPreset, customActiveTools: m.customActiveTools ?? null, ts: Date.now() });
|
|
857
902
|
}
|
|
858
903
|
|
|
904
|
+
function setWorkflow(m: Managed, next: WorkflowState): void {
|
|
905
|
+
saveWorkflow(m.sm, next);
|
|
906
|
+
m.workflow = next;
|
|
907
|
+
publishWorkflow(m);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function publishWorkflow(m: Managed): void {
|
|
911
|
+
if (m.session && !m.disposed) {
|
|
912
|
+
applyToolPolicy(m, m.session);
|
|
913
|
+
publishTools(m, m.session);
|
|
914
|
+
}
|
|
915
|
+
publish(m, { type: "workflow", workflow: m.workflow, ts: Date.now() });
|
|
916
|
+
}
|
|
917
|
+
|
|
859
918
|
/** SDK reload 自动启用扩展工具,注册新工具也会刷新全集;统一在激活边界收敛到服务端选择。 */
|
|
860
919
|
function installSessionPolicy(m: Managed, session: AgentSession): void {
|
|
861
920
|
const setActive = session.setActiveToolsByName.bind(session);
|
|
@@ -1171,12 +1230,12 @@ export async function buildSnapshot(m: Managed): Promise<WebSnapshot> {
|
|
|
1171
1230
|
}
|
|
1172
1231
|
for (let i = messages.length - 1; i >= 0 && !compactedSinceLastAssistant; i--) {
|
|
1173
1232
|
const msg = messages[i];
|
|
1174
|
-
if (msg.role === "assistant" && msg.usage) {
|
|
1175
|
-
lastTurnTokens =
|
|
1233
|
+
if (msg.role === "assistant" && msg.usage && msg.stopReason !== "error" && msg.stopReason !== "aborted") {
|
|
1234
|
+
lastTurnTokens = Number(msg.usage.totalTokens ?? 0) || (
|
|
1176
1235
|
Number(msg.usage.input ?? 0) +
|
|
1177
1236
|
Number(msg.usage.cacheRead ?? 0) +
|
|
1178
1237
|
Number(msg.usage.cacheWrite ?? 0) +
|
|
1179
|
-
Number(msg.usage.output ?? 0);
|
|
1238
|
+
Number(msg.usage.output ?? 0));
|
|
1180
1239
|
break;
|
|
1181
1240
|
}
|
|
1182
1241
|
}
|
|
@@ -1241,6 +1300,7 @@ export async function buildSnapshot(m: Managed): Promise<WebSnapshot> {
|
|
|
1241
1300
|
thinkingLevel,
|
|
1242
1301
|
thinkingLevels,
|
|
1243
1302
|
toolPreset: m.toolPreset,
|
|
1303
|
+
workflow: m.workflow,
|
|
1244
1304
|
customActiveTools: m.customActiveTools ?? null,
|
|
1245
1305
|
extensionUiRequests: m.ui?.getPendingRequests() ?? [],
|
|
1246
1306
|
tools,
|
|
@@ -1294,12 +1354,52 @@ export interface CommandResult {
|
|
|
1294
1354
|
}
|
|
1295
1355
|
|
|
1296
1356
|
export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandResult> {
|
|
1357
|
+
let release: (() => void) | undefined;
|
|
1358
|
+
try {
|
|
1359
|
+
if (!["abort", "clearQueue", "extensionUiResponse"].includes(cmd.cmd)) release = beginActivity("session");
|
|
1360
|
+
return await executeCommand(m, cmd);
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
1363
|
+
} finally {
|
|
1364
|
+
release?.();
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
function trackedPrompt(session: AgentSession, ...args: Parameters<AgentSession["prompt"]>): ReturnType<AgentSession["prompt"]> {
|
|
1369
|
+
const release = beginActivity("session");
|
|
1370
|
+
try { return session.prompt(...args).finally(release); }
|
|
1371
|
+
catch (error) { release(); throw error; }
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
async function executeCommand(m: Managed, cmd: AgentCommand): Promise<CommandResult> {
|
|
1297
1375
|
touch(m);
|
|
1298
1376
|
try {
|
|
1299
1377
|
switch (cmd.cmd) {
|
|
1300
1378
|
case "prepare":
|
|
1301
1379
|
await ensureSession(m);
|
|
1302
1380
|
return { ok: true };
|
|
1381
|
+
case "setWorkflowMode": {
|
|
1382
|
+
if (!isWorkflowMode(cmd.mode)) return { ok: false, error: "invalid workflow mode" };
|
|
1383
|
+
if (isManagedBusy(m)) return { ok: false, error: "cannot change workflow while the agent is running" };
|
|
1384
|
+
// 重选当前模式不重置:否则会清掉 Goal 目标或待执行的方案,并多写一条记录。
|
|
1385
|
+
if (cmd.mode === m.workflow.mode) return { ok: true };
|
|
1386
|
+
setWorkflow(m, { mode: cmd.mode, planStatus: "idle", goal: "" });
|
|
1387
|
+
return { ok: true };
|
|
1388
|
+
}
|
|
1389
|
+
case "approvePlan": {
|
|
1390
|
+
if (!planCanRun(m.workflow)) return { ok: false, error: "there is no plan awaiting approval" };
|
|
1391
|
+
if (isManagedBusy(m)) return { ok: false, error: "session is busy" };
|
|
1392
|
+
const branch = m.sm.getBranch();
|
|
1393
|
+
const planIndex = branch.findIndex((entry) => entry.id === m.workflow.planId);
|
|
1394
|
+
if (planIndex < 0 || branch.slice(planIndex + 1).some((entry) => entry.type === "message")) {
|
|
1395
|
+
return { ok: false, error: "the plan is no longer the latest conversation; request a new plan" };
|
|
1396
|
+
}
|
|
1397
|
+
const ready = m.workflow;
|
|
1398
|
+
setWorkflow(m, { ...ready, planStatus: "executing" });
|
|
1399
|
+
const result = await execute(m, { cmd: "prompt", text: "Execute the plan you just proposed. Verify the changes and report any unfinished work." });
|
|
1400
|
+
if (!result.ok) setWorkflow(m, ready);
|
|
1401
|
+
return result;
|
|
1402
|
+
}
|
|
1303
1403
|
case "prompt": {
|
|
1304
1404
|
const text = (cmd.text ?? "").trim();
|
|
1305
1405
|
if (!text && !cmd.images?.length) return { ok: false, error: "empty prompt" };
|
|
@@ -1311,6 +1411,7 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1311
1411
|
if (m.disposed) throw new Error("session is disposed");
|
|
1312
1412
|
// 只有用户显式选择 steer/followUp 才排队;旧的普通提交不能悄悄成为 steering。
|
|
1313
1413
|
if ((session.isStreaming || m.runActive) && !cmd.behavior) return { ok: false, error: "session is busy; choose steer or followUp" };
|
|
1414
|
+
if (m.workflow.mode === "plan" && (session.isStreaming || m.runActive)) return { ok: false, error: "wait for the current plan or execution to finish" };
|
|
1314
1415
|
if (!session.isStreaming) {
|
|
1315
1416
|
// 基线快照不能无限阻塞首条 prompt(git 不可用时每次探测至多 30s 超时):
|
|
1316
1417
|
// 5s 内没完成就放行 prompt,快照后台补拍(工具结束后照常记步,外部修改兜底)。
|
|
@@ -1320,11 +1421,18 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1320
1421
|
]);
|
|
1321
1422
|
}
|
|
1322
1423
|
if (m.disposed) throw new Error("session is disposed");
|
|
1323
|
-
|
|
1424
|
+
const previousWorkflow = m.workflow;
|
|
1425
|
+
if (m.workflow.mode === "plan" && m.workflow.planStatus !== "executing") {
|
|
1426
|
+
setWorkflow(m, { ...m.workflow, planStatus: "planning", planId: undefined });
|
|
1427
|
+
} else if (m.workflow.mode === "goal" && !m.workflow.goal && text) {
|
|
1428
|
+
setWorkflow(m, { ...m.workflow, goal: text.slice(0, 8_000) });
|
|
1429
|
+
}
|
|
1430
|
+
const result = await new Promise<CommandResult>((resolve) => {
|
|
1324
1431
|
let accepted = false;
|
|
1325
|
-
const run = session
|
|
1432
|
+
const run = trackedPrompt(session, text, {
|
|
1326
1433
|
images: cmd.images,
|
|
1327
1434
|
streamingBehavior: cmd.behavior,
|
|
1435
|
+
expandPromptTemplates: m.workflow.mode !== "plan" || m.workflow.planStatus === "executing",
|
|
1328
1436
|
source: "rpc",
|
|
1329
1437
|
preflightResult: (success) => {
|
|
1330
1438
|
if (success) {
|
|
@@ -1341,7 +1449,9 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1341
1449
|
if (accepted) publish(m, { type: "error", message, ts: Date.now() });
|
|
1342
1450
|
else resolve({ ok: false, error: message });
|
|
1343
1451
|
});
|
|
1344
|
-
|
|
1452
|
+
});
|
|
1453
|
+
if (!result.ok && m.workflow !== previousWorkflow) setWorkflow(m, previousWorkflow);
|
|
1454
|
+
return result;
|
|
1345
1455
|
} finally {
|
|
1346
1456
|
m.promptSubmitting = false;
|
|
1347
1457
|
if (!m.disposed) publish(m, { type: "status", isStreaming: isManagedBusy(m), state: isManagedBusy(m) ? "running" : "idle", ts: Date.now() });
|
|
@@ -1350,6 +1460,7 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1350
1460
|
}
|
|
1351
1461
|
case "steer":
|
|
1352
1462
|
case "followUp": {
|
|
1463
|
+
if (m.workflow.mode === "plan") return { ok: false, error: "Plan mode does not queue prompts" };
|
|
1353
1464
|
if (m.promptSubmitting || m.resourceReloading) return { ok: false, error: "session is busy accepting another command" };
|
|
1354
1465
|
const session = await ensureSession(m);
|
|
1355
1466
|
if (m.promptSubmitting || m.resourceReloading || !session.isStreaming) return { ok: false, error: "session is not accepting queued messages" };
|
|
@@ -1438,6 +1549,10 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1438
1549
|
const entryId = String((cmd as any).entryId ?? "");
|
|
1439
1550
|
if (!entryId) return { ok: false, error: "missing entryId" };
|
|
1440
1551
|
const r = await session.navigateTree(entryId);
|
|
1552
|
+
if (!r?.cancelled) {
|
|
1553
|
+
m.workflow = readWorkflow(m.sm);
|
|
1554
|
+
publishWorkflow(m);
|
|
1555
|
+
}
|
|
1441
1556
|
return { ok: !r?.cancelled, data: { cancelled: r?.cancelled === true, editorText: r?.editorText } };
|
|
1442
1557
|
}
|
|
1443
1558
|
case "editAndResend": {
|
|
@@ -1447,6 +1562,9 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1447
1562
|
const text = (cmd.text ?? "").trim();
|
|
1448
1563
|
if (!entryId) return { ok: false, error: "missing entryId" };
|
|
1449
1564
|
if (!text) return { ok: false, error: "empty prompt" };
|
|
1565
|
+
const target = m.sm.getEntry(entryId);
|
|
1566
|
+
if (target?.type !== "message" || target.message.role !== "user") return { ok: false, error: "message is not editable" };
|
|
1567
|
+
const images = Array.isArray(target.message.content) ? target.message.content.filter((part) => part.type === "image") : [];
|
|
1450
1568
|
if (m.promptSubmitting || m.resourceReloading) return { ok: false, error: "session is busy accepting another command" };
|
|
1451
1569
|
m.promptSubmitting = true;
|
|
1452
1570
|
try {
|
|
@@ -1460,11 +1578,64 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1460
1578
|
}
|
|
1461
1579
|
}
|
|
1462
1580
|
if (m.disposed) throw new Error("session is disposed");
|
|
1463
|
-
|
|
1581
|
+
// SDK 对当前叶节点直接返回 no-op;先移开叶节点,才能把最后一条用户消息也原位重发。
|
|
1582
|
+
const originalLeafId = m.sm.getLeafId();
|
|
1583
|
+
const leafWasTarget = originalLeafId === entryId;
|
|
1584
|
+
if (leafWasTarget) {
|
|
1585
|
+
if (target.parentId) m.sm.branch(target.parentId);
|
|
1586
|
+
else m.sm.resetLeaf();
|
|
1587
|
+
}
|
|
1588
|
+
let moved;
|
|
1589
|
+
try {
|
|
1590
|
+
moved = await session.navigateTree(entryId);
|
|
1591
|
+
} catch (error) {
|
|
1592
|
+
if (leafWasTarget) m.sm.branch(entryId);
|
|
1593
|
+
throw error;
|
|
1594
|
+
}
|
|
1595
|
+
if (moved?.cancelled && leafWasTarget) m.sm.branch(entryId);
|
|
1464
1596
|
if (moved?.cancelled) return { ok: false, error: "edit cancelled" };
|
|
1597
|
+
m.workflow = readWorkflow(m.sm);
|
|
1598
|
+
publishWorkflow(m);
|
|
1465
1599
|
await growthOf(m).prepare().catch(() => undefined);
|
|
1466
|
-
|
|
1467
|
-
|
|
1600
|
+
const previousWorkflow = m.workflow;
|
|
1601
|
+
if (m.workflow.mode === "plan") setWorkflow(m, { ...m.workflow, planStatus: "planning", planId: undefined });
|
|
1602
|
+
// 编辑的正是定下目标的那条消息时,目标跟着改;旧目标记录是它的祖先,回退分支不会带走它。
|
|
1603
|
+
else if (m.workflow.mode === "goal" && awaitingGoalMessage(m.sm) && m.workflow.goal !== text.slice(0, 8_000)) {
|
|
1604
|
+
setWorkflow(m, { ...m.workflow, goal: text.slice(0, 8_000) });
|
|
1605
|
+
}
|
|
1606
|
+
const options = { images, expandPromptTemplates: m.workflow.mode !== "plan" };
|
|
1607
|
+
return await new Promise<CommandResult>((resolve) => {
|
|
1608
|
+
let accepted = false;
|
|
1609
|
+
const run = trackedPrompt(session, text, {
|
|
1610
|
+
...options,
|
|
1611
|
+
preflightResult: (success) => {
|
|
1612
|
+
if (success) {
|
|
1613
|
+
accepted = true;
|
|
1614
|
+
resolve({ ok: true, data: { accepted: true } });
|
|
1615
|
+
}
|
|
1616
|
+
},
|
|
1617
|
+
});
|
|
1618
|
+
void run.then(() => {
|
|
1619
|
+
if (!accepted) resolve({ ok: true });
|
|
1620
|
+
}, (error) => {
|
|
1621
|
+
const message = String(error?.message ?? error);
|
|
1622
|
+
if (accepted) {
|
|
1623
|
+
publish(m, { type: "error", message, ts: Date.now() });
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
// Rejected preflight never wrote the edited turn; restore the old branch before the client resyncs.
|
|
1627
|
+
try {
|
|
1628
|
+
if (originalLeafId) m.sm.branch(originalLeafId);
|
|
1629
|
+
else m.sm.resetLeaf();
|
|
1630
|
+
session.refreshContext();
|
|
1631
|
+
m.workflow = readWorkflow(m.sm);
|
|
1632
|
+
publishWorkflow(m);
|
|
1633
|
+
} catch {
|
|
1634
|
+
if (m.workflow !== previousWorkflow) setWorkflow(m, previousWorkflow);
|
|
1635
|
+
}
|
|
1636
|
+
resolve({ ok: false, error: message });
|
|
1637
|
+
});
|
|
1638
|
+
});
|
|
1468
1639
|
} finally {
|
|
1469
1640
|
m.promptSubmitting = false;
|
|
1470
1641
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentCommand, ImageAttachment, ToolPreset } from "./types";
|
|
1
|
+
import type { AgentCommand, ImageAttachment, ToolPreset, WorkflowMode } from "./types";
|
|
2
2
|
|
|
3
3
|
const COMMANDS = new Set<AgentCommand["cmd"]>([
|
|
4
4
|
"prepare",
|
|
@@ -10,6 +10,8 @@ const COMMANDS = new Set<AgentCommand["cmd"]>([
|
|
|
10
10
|
"setModel",
|
|
11
11
|
"setThinkingLevel",
|
|
12
12
|
"setToolPreset",
|
|
13
|
+
"setWorkflowMode",
|
|
14
|
+
"approvePlan",
|
|
13
15
|
"setActiveTools",
|
|
14
16
|
"rename",
|
|
15
17
|
"fork",
|
|
@@ -21,6 +23,7 @@ const COMMANDS = new Set<AgentCommand["cmd"]>([
|
|
|
21
23
|
"reload",
|
|
22
24
|
]);
|
|
23
25
|
const TOOL_PRESETS = new Set<ToolPreset>(["readonly", "standard", "full"]);
|
|
26
|
+
const WORKFLOW_MODES = new Set<WorkflowMode>(["agent", "plan", "goal"]);
|
|
24
27
|
const STREAMING_BEHAVIORS = new Set(["steer", "followUp"]);
|
|
25
28
|
const IMAGE_TYPES = new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]);
|
|
26
29
|
const MAX_TEXT = 1_000_000;
|
|
@@ -78,6 +81,10 @@ export function parseAgentCommand(value: unknown): CommandParseResult {
|
|
|
78
81
|
if (typeof raw.preset !== "string" || !TOOL_PRESETS.has(raw.preset as ToolPreset)) throw new Error("invalid tool preset");
|
|
79
82
|
command.preset = raw.preset as ToolPreset;
|
|
80
83
|
}
|
|
84
|
+
if (raw.mode !== undefined) {
|
|
85
|
+
if (typeof raw.mode !== "string" || !WORKFLOW_MODES.has(raw.mode as WorkflowMode)) throw new Error("invalid workflow mode");
|
|
86
|
+
command.mode = raw.mode as WorkflowMode;
|
|
87
|
+
}
|
|
81
88
|
if (raw.behavior !== undefined) {
|
|
82
89
|
if (typeof raw.behavior !== "string" || !STREAMING_BEHAVIORS.has(raw.behavior)) throw new Error("invalid streaming behavior");
|
|
83
90
|
command.behavior = raw.behavior as "steer" | "followUp";
|
|
@@ -108,6 +115,7 @@ export function parseAgentCommand(value: unknown): CommandParseResult {
|
|
|
108
115
|
if (command.cmd === "setModel" && (!command.provider || !command.modelId)) throw new Error("provider and modelId are required");
|
|
109
116
|
if (command.cmd === "setThinkingLevel" && !command.level) throw new Error("level is required");
|
|
110
117
|
if (command.cmd === "setToolPreset" && !command.preset) throw new Error("preset is required");
|
|
118
|
+
if (command.cmd === "setWorkflowMode" && !command.mode) throw new Error("mode is required");
|
|
111
119
|
if (command.cmd === "setActiveTools" && !command.names) throw new Error("names are required");
|
|
112
120
|
if (command.cmd === "navigate" && !command.entryId) throw new Error("entryId is required");
|
|
113
121
|
return { ok: true, command };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { DefaultResourceLoader, SessionManager, createAgentSession, defineTool, type AgentSession, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { getAgentDir, getModelRuntime, getSettingsManager, TOOL_PRESETS, withResourceLock } from "./pi";
|
|
4
|
+
|
|
5
|
+
const MAX_TASKS = 3;
|
|
6
|
+
const TASK_TIMEOUT_MS = 120_000;
|
|
7
|
+
const MAX_REPORT_CHARS = 12_000;
|
|
8
|
+
|
|
9
|
+
const parameters = Type.Object({
|
|
10
|
+
tasks: Type.Array(Type.Object({
|
|
11
|
+
title: Type.String({ minLength: 1, maxLength: 80 }),
|
|
12
|
+
instruction: Type.String({ minLength: 1, maxLength: 4_000 }),
|
|
13
|
+
}), { minItems: 1, maxItems: MAX_TASKS }),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function reportOf(session: AgentSession): string {
|
|
17
|
+
const answer = [...session.messages].reverse().find((message) => message.role === "assistant");
|
|
18
|
+
if (!answer || answer.role !== "assistant") return "The subagent returned no assistant answer.";
|
|
19
|
+
return answer.content.filter((part) => part.type === "text").map((part) => part.text).join("\n").slice(0, MAX_REPORT_CHARS) || "The subagent returned no text answer.";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function runSubagent(cwd: string, parent: AgentSession, instruction: string, signal?: AbortSignal): Promise<string> {
|
|
23
|
+
if (signal?.aborted) throw new Error("Delegation cancelled");
|
|
24
|
+
const loader = new DefaultResourceLoader({
|
|
25
|
+
cwd,
|
|
26
|
+
agentDir: getAgentDir(),
|
|
27
|
+
settingsManager: getSettingsManager(cwd),
|
|
28
|
+
noExtensions: true,
|
|
29
|
+
noPromptTemplates: true,
|
|
30
|
+
noThemes: true,
|
|
31
|
+
appendSystemPrompt: ["You are a read-only PiWeb subagent. Investigate the assigned task and return a concise, evidence-based report. Do not modify files or run commands. Do not delegate further."],
|
|
32
|
+
});
|
|
33
|
+
let child: AgentSession | undefined;
|
|
34
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
35
|
+
let timedOut = false;
|
|
36
|
+
const abort = () => { void child?.abort().catch(() => undefined); };
|
|
37
|
+
try {
|
|
38
|
+
await withResourceLock(() => loader.reload());
|
|
39
|
+
if (signal?.aborted) throw new Error("Delegation cancelled");
|
|
40
|
+
const created = await createAgentSession({
|
|
41
|
+
cwd,
|
|
42
|
+
agentDir: getAgentDir(),
|
|
43
|
+
sessionManager: SessionManager.inMemory(cwd),
|
|
44
|
+
modelRuntime: await getModelRuntime(),
|
|
45
|
+
model: parent.model ?? undefined,
|
|
46
|
+
thinkingLevel: parent.thinkingLevel,
|
|
47
|
+
settingsManager: getSettingsManager(cwd),
|
|
48
|
+
resourceLoader: loader,
|
|
49
|
+
tools: TOOL_PRESETS.readonly,
|
|
50
|
+
});
|
|
51
|
+
child = created.session;
|
|
52
|
+
if (signal?.aborted) throw new Error("Delegation cancelled");
|
|
53
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
54
|
+
timer = setTimeout(() => { timedOut = true; abort(); }, TASK_TIMEOUT_MS);
|
|
55
|
+
await child.prompt(instruction, { source: "rpc", expandPromptTemplates: false });
|
|
56
|
+
if (timedOut) throw new Error("Subagent timed out after two minutes");
|
|
57
|
+
if (signal?.aborted) throw new Error("Delegation cancelled");
|
|
58
|
+
return reportOf(child);
|
|
59
|
+
} finally {
|
|
60
|
+
if (timer) clearTimeout(timer);
|
|
61
|
+
signal?.removeEventListener("abort", abort);
|
|
62
|
+
child?.dispose();
|
|
63
|
+
loader.getExtensions().runtime.invalidate();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createDelegationTool(cwd: string, parent: () => AgentSession | null): ToolDefinition {
|
|
68
|
+
return defineTool({
|
|
69
|
+
name: "piweb_delegate",
|
|
70
|
+
label: "Delegate research",
|
|
71
|
+
description: "Delegate one to three independent, read-only research or review tasks to temporary Pi subagents. They cannot edit files, run shell commands, or delegate further. Use only when parallel investigation is useful; summarize their findings yourself.",
|
|
72
|
+
promptSnippet: "Delegate bounded, read-only research tasks to temporary subagents.",
|
|
73
|
+
parameters,
|
|
74
|
+
executionMode: "sequential",
|
|
75
|
+
async execute(_id, { tasks }, signal, onUpdate) {
|
|
76
|
+
const leader = parent();
|
|
77
|
+
if (!leader) throw new Error("Parent session is unavailable");
|
|
78
|
+
const reports: string[] = new Array(tasks.length);
|
|
79
|
+
await Promise.all(tasks.map(async (task, index) => {
|
|
80
|
+
try {
|
|
81
|
+
const report = await runSubagent(cwd, leader, task.instruction, signal);
|
|
82
|
+
reports[index] = `## ${task.title}\n${report}`;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
reports[index] = `## ${task.title}\nFailed: ${String((error as Error)?.message ?? error)}`;
|
|
85
|
+
}
|
|
86
|
+
onUpdate?.({ content: [{ type: "text", text: reports.filter(Boolean).join("\n\n") }], details: undefined });
|
|
87
|
+
}));
|
|
88
|
+
if (signal?.aborted) throw new Error("Delegation cancelled");
|
|
89
|
+
const text = reports.join("\n\n");
|
|
90
|
+
return { content: [{ type: "text", text }], details: undefined };
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|