@rexvane/piweb 0.3.15 → 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 +9 -6
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +6 -6
- package/.next/react-loadable-manifest.json +16 -4
- 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 +9 -6
- 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 +10 -0
- package/package.json +4 -3
- package/scripts/pick-folder.ps1 +11 -2
- package/scripts/release.mjs +11 -31
- package/scripts/run-test-file.mjs +34 -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 +17 -17
- 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 +180 -12
- package/src/lib/command-validation.ts +9 -1
- package/src/lib/delegation.ts +93 -0
- package/src/lib/managed-niubash.ts +8 -4
- package/src/lib/pi.ts +3 -2
- package/src/lib/runtime-activity.ts +27 -0
- package/src/lib/session-import/index.ts +5 -0
- package/src/lib/session-import/lock.ts +25 -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/DdBwPYoHBKtaJ97QIr6Co/_buildManifest.js +0 -1
- package/.next/static/chunks/2608.ec8ff1f5767c9056.js +0 -1
- package/.next/static/chunks/4632.0ed92ae42905ce40.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-42c5aae79b0e5d8c.js +0 -1
- package/.next/static/css/b2d35abc828d4713.css +0 -1
- /package/.next/static/{DdBwPYoHBKtaJ97QIr6Co → 0t1jBUAuVaTFTXxRyIo3q}/_ssgManifest.js +0 -0
package/src/lib/agent-manager.ts
CHANGED
|
@@ -33,9 +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";
|
|
38
39
|
import { createManagedNiubashTools } from "./managed-niubash";
|
|
40
|
+
import { createDelegationTool } from "./delegation";
|
|
41
|
+
import { awaitingGoalMessage, isWorkflowMode, planCanRun, readWorkflow, saveWorkflow, workflowInstruction, type WorkflowState } from "./workflow";
|
|
39
42
|
import { clampThinkingLevel, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
40
43
|
import type {
|
|
41
44
|
AgentCommand,
|
|
@@ -66,6 +69,7 @@ interface Managed {
|
|
|
66
69
|
ledger: TrajLedger;
|
|
67
70
|
lastActive: number;
|
|
68
71
|
toolPreset: ToolPreset;
|
|
72
|
+
workflow: WorkflowState;
|
|
69
73
|
/** 显式逐项选择优先于 preset;保留失效名称以便资源重新启用后恢复用户选择。 */
|
|
70
74
|
customActiveTools?: string[];
|
|
71
75
|
resourceVersion: number;
|
|
@@ -527,6 +531,20 @@ function translate(m: Managed, evt: AgentSessionEvent): void {
|
|
|
527
531
|
break;
|
|
528
532
|
case "agent_settled":
|
|
529
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
|
+
}
|
|
530
548
|
publish(m, { type: "status", isStreaming: false, state: "idle", ts: now });
|
|
531
549
|
publishUsage(m);
|
|
532
550
|
if (m.resourceReloadPending) queueMicrotask(() => void reloadManagedResources(m));
|
|
@@ -636,6 +654,7 @@ function registerManaged(sessionPath: string, cwd: string, sm: SessionManager):
|
|
|
636
654
|
ledger: new TrajLedger(initialTrajectory),
|
|
637
655
|
lastActive: Date.now(),
|
|
638
656
|
toolPreset: "readonly",
|
|
657
|
+
workflow: readWorkflow(sm),
|
|
639
658
|
resourceVersion: 0,
|
|
640
659
|
resourceReloadPending: false,
|
|
641
660
|
resourceReloading: false,
|
|
@@ -784,12 +803,33 @@ function publishEnvironmentTrajectory(m: Managed, session: AgentSession): void {
|
|
|
784
803
|
}
|
|
785
804
|
|
|
786
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> {
|
|
787
812
|
if (m.disposed) throw new Error("session is disposed");
|
|
788
813
|
if (m.creating) return m.creating;
|
|
789
814
|
if (m.session) return m.session;
|
|
790
815
|
m.creating = (async () => {
|
|
791
816
|
await resourceLoaderReady(m.cwd);
|
|
792
|
-
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
|
+
}]);
|
|
793
833
|
let created: AgentSession | undefined;
|
|
794
834
|
try {
|
|
795
835
|
if (m.disposed) throw new Error("session is disposed");
|
|
@@ -801,7 +841,7 @@ export async function ensureSession(m: Managed): Promise<AgentSession> {
|
|
|
801
841
|
resourceLoader: loader,
|
|
802
842
|
settingsManager: getSettingsManager(m.cwd),
|
|
803
843
|
// Windows: 同名 bash 覆盖 SDK 内置工具,指向托管 niubash;其它平台返回 [],走 Pi 默认。
|
|
804
|
-
customTools: createManagedNiubashTools(m.cwd),
|
|
844
|
+
customTools: [...createManagedNiubashTools(m.cwd), createDelegationTool(m.cwd, () => m.session)],
|
|
805
845
|
});
|
|
806
846
|
created = session;
|
|
807
847
|
if (m.disposed) throw new Error("session is disposed");
|
|
@@ -848,7 +888,9 @@ function isManagedBusy(m: Managed): boolean {
|
|
|
848
888
|
|
|
849
889
|
function allowedToolNames(m: Managed, session: AgentSession): string[] {
|
|
850
890
|
const allowed = m.customActiveTools ?? (m.toolPreset === "full" ? undefined : TOOL_PRESETS[m.toolPreset]);
|
|
851
|
-
|
|
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)));
|
|
852
894
|
}
|
|
853
895
|
|
|
854
896
|
function applyToolPolicy(m: Managed, session: AgentSession): void {
|
|
@@ -859,6 +901,20 @@ function publishTools(m: Managed, session: AgentSession): void {
|
|
|
859
901
|
publish(m, { type: "tools", active: listActiveTools(session), all: listAllToolNames(session), toolPreset: m.toolPreset, customActiveTools: m.customActiveTools ?? null, ts: Date.now() });
|
|
860
902
|
}
|
|
861
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
|
+
|
|
862
918
|
/** SDK reload 自动启用扩展工具,注册新工具也会刷新全集;统一在激活边界收敛到服务端选择。 */
|
|
863
919
|
function installSessionPolicy(m: Managed, session: AgentSession): void {
|
|
864
920
|
const setActive = session.setActiveToolsByName.bind(session);
|
|
@@ -1174,12 +1230,12 @@ export async function buildSnapshot(m: Managed): Promise<WebSnapshot> {
|
|
|
1174
1230
|
}
|
|
1175
1231
|
for (let i = messages.length - 1; i >= 0 && !compactedSinceLastAssistant; i--) {
|
|
1176
1232
|
const msg = messages[i];
|
|
1177
|
-
if (msg.role === "assistant" && msg.usage) {
|
|
1178
|
-
lastTurnTokens =
|
|
1233
|
+
if (msg.role === "assistant" && msg.usage && msg.stopReason !== "error" && msg.stopReason !== "aborted") {
|
|
1234
|
+
lastTurnTokens = Number(msg.usage.totalTokens ?? 0) || (
|
|
1179
1235
|
Number(msg.usage.input ?? 0) +
|
|
1180
1236
|
Number(msg.usage.cacheRead ?? 0) +
|
|
1181
1237
|
Number(msg.usage.cacheWrite ?? 0) +
|
|
1182
|
-
Number(msg.usage.output ?? 0);
|
|
1238
|
+
Number(msg.usage.output ?? 0));
|
|
1183
1239
|
break;
|
|
1184
1240
|
}
|
|
1185
1241
|
}
|
|
@@ -1244,6 +1300,7 @@ export async function buildSnapshot(m: Managed): Promise<WebSnapshot> {
|
|
|
1244
1300
|
thinkingLevel,
|
|
1245
1301
|
thinkingLevels,
|
|
1246
1302
|
toolPreset: m.toolPreset,
|
|
1303
|
+
workflow: m.workflow,
|
|
1247
1304
|
customActiveTools: m.customActiveTools ?? null,
|
|
1248
1305
|
extensionUiRequests: m.ui?.getPendingRequests() ?? [],
|
|
1249
1306
|
tools,
|
|
@@ -1297,12 +1354,52 @@ export interface CommandResult {
|
|
|
1297
1354
|
}
|
|
1298
1355
|
|
|
1299
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> {
|
|
1300
1375
|
touch(m);
|
|
1301
1376
|
try {
|
|
1302
1377
|
switch (cmd.cmd) {
|
|
1303
1378
|
case "prepare":
|
|
1304
1379
|
await ensureSession(m);
|
|
1305
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
|
+
}
|
|
1306
1403
|
case "prompt": {
|
|
1307
1404
|
const text = (cmd.text ?? "").trim();
|
|
1308
1405
|
if (!text && !cmd.images?.length) return { ok: false, error: "empty prompt" };
|
|
@@ -1314,6 +1411,7 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1314
1411
|
if (m.disposed) throw new Error("session is disposed");
|
|
1315
1412
|
// 只有用户显式选择 steer/followUp 才排队;旧的普通提交不能悄悄成为 steering。
|
|
1316
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" };
|
|
1317
1415
|
if (!session.isStreaming) {
|
|
1318
1416
|
// 基线快照不能无限阻塞首条 prompt(git 不可用时每次探测至多 30s 超时):
|
|
1319
1417
|
// 5s 内没完成就放行 prompt,快照后台补拍(工具结束后照常记步,外部修改兜底)。
|
|
@@ -1323,11 +1421,18 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1323
1421
|
]);
|
|
1324
1422
|
}
|
|
1325
1423
|
if (m.disposed) throw new Error("session is disposed");
|
|
1326
|
-
|
|
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) => {
|
|
1327
1431
|
let accepted = false;
|
|
1328
|
-
const run = session
|
|
1432
|
+
const run = trackedPrompt(session, text, {
|
|
1329
1433
|
images: cmd.images,
|
|
1330
1434
|
streamingBehavior: cmd.behavior,
|
|
1435
|
+
expandPromptTemplates: m.workflow.mode !== "plan" || m.workflow.planStatus === "executing",
|
|
1331
1436
|
source: "rpc",
|
|
1332
1437
|
preflightResult: (success) => {
|
|
1333
1438
|
if (success) {
|
|
@@ -1344,7 +1449,9 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1344
1449
|
if (accepted) publish(m, { type: "error", message, ts: Date.now() });
|
|
1345
1450
|
else resolve({ ok: false, error: message });
|
|
1346
1451
|
});
|
|
1347
|
-
|
|
1452
|
+
});
|
|
1453
|
+
if (!result.ok && m.workflow !== previousWorkflow) setWorkflow(m, previousWorkflow);
|
|
1454
|
+
return result;
|
|
1348
1455
|
} finally {
|
|
1349
1456
|
m.promptSubmitting = false;
|
|
1350
1457
|
if (!m.disposed) publish(m, { type: "status", isStreaming: isManagedBusy(m), state: isManagedBusy(m) ? "running" : "idle", ts: Date.now() });
|
|
@@ -1353,6 +1460,7 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1353
1460
|
}
|
|
1354
1461
|
case "steer":
|
|
1355
1462
|
case "followUp": {
|
|
1463
|
+
if (m.workflow.mode === "plan") return { ok: false, error: "Plan mode does not queue prompts" };
|
|
1356
1464
|
if (m.promptSubmitting || m.resourceReloading) return { ok: false, error: "session is busy accepting another command" };
|
|
1357
1465
|
const session = await ensureSession(m);
|
|
1358
1466
|
if (m.promptSubmitting || m.resourceReloading || !session.isStreaming) return { ok: false, error: "session is not accepting queued messages" };
|
|
@@ -1441,6 +1549,10 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1441
1549
|
const entryId = String((cmd as any).entryId ?? "");
|
|
1442
1550
|
if (!entryId) return { ok: false, error: "missing entryId" };
|
|
1443
1551
|
const r = await session.navigateTree(entryId);
|
|
1552
|
+
if (!r?.cancelled) {
|
|
1553
|
+
m.workflow = readWorkflow(m.sm);
|
|
1554
|
+
publishWorkflow(m);
|
|
1555
|
+
}
|
|
1444
1556
|
return { ok: !r?.cancelled, data: { cancelled: r?.cancelled === true, editorText: r?.editorText } };
|
|
1445
1557
|
}
|
|
1446
1558
|
case "editAndResend": {
|
|
@@ -1450,6 +1562,9 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1450
1562
|
const text = (cmd.text ?? "").trim();
|
|
1451
1563
|
if (!entryId) return { ok: false, error: "missing entryId" };
|
|
1452
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") : [];
|
|
1453
1568
|
if (m.promptSubmitting || m.resourceReloading) return { ok: false, error: "session is busy accepting another command" };
|
|
1454
1569
|
m.promptSubmitting = true;
|
|
1455
1570
|
try {
|
|
@@ -1463,11 +1578,64 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1463
1578
|
}
|
|
1464
1579
|
}
|
|
1465
1580
|
if (m.disposed) throw new Error("session is disposed");
|
|
1466
|
-
|
|
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);
|
|
1467
1596
|
if (moved?.cancelled) return { ok: false, error: "edit cancelled" };
|
|
1597
|
+
m.workflow = readWorkflow(m.sm);
|
|
1598
|
+
publishWorkflow(m);
|
|
1468
1599
|
await growthOf(m).prepare().catch(() => undefined);
|
|
1469
|
-
|
|
1470
|
-
|
|
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
|
+
});
|
|
1471
1639
|
} finally {
|
|
1472
1640
|
m.promptSubmitting = false;
|
|
1473
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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
-
import
|
|
3
|
+
import * as nodeModule from "node:module";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createBashToolDefinition, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { APP_ROOT } from "./app-root";
|
|
@@ -56,12 +56,16 @@ type LoadedPackage = { entryPath: string; value: unknown };
|
|
|
56
56
|
* 这与平台包发布后的目录布局一一对应,不引入第二套解析规则。
|
|
57
57
|
*/
|
|
58
58
|
export function resolveManagedNiubashPackageEntry(packageName: string, arch: SupportedArch, root = APP_ROOT): LoadedPackage {
|
|
59
|
-
|
|
59
|
+
// 必须通过命名空间调用。webpack 会改写从 node:module 具名导入的 createRequire:
|
|
60
|
+
// 参数不是字符串字面量时把调用擦掉,运行时变成对 undefined 调用 resolve。
|
|
61
|
+
const loadPackage = nodeModule.createRequire(path.join(root, "package.json"));
|
|
62
|
+
const resolveEntry = loadPackage.resolve.bind(loadPackage);
|
|
60
63
|
try {
|
|
61
|
-
|
|
64
|
+
const entryPath = resolveEntry(packageName);
|
|
65
|
+
return { entryPath, value: loadPackage(packageName) };
|
|
62
66
|
} catch (error) {
|
|
63
67
|
const checkoutEntry = path.join(root, "platform-packages", PACKAGE_BY_ARCH[arch].directory, "index.cjs");
|
|
64
|
-
if (fs.existsSync(checkoutEntry)) return { entryPath: checkoutEntry, value:
|
|
68
|
+
if (fs.existsSync(checkoutEntry)) return { entryPath: checkoutEntry, value: loadPackage(checkoutEntry) };
|
|
65
69
|
throw error;
|
|
66
70
|
}
|
|
67
71
|
}
|
package/src/lib/pi.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type SettingsManager,
|
|
10
10
|
DefaultPackageManager,
|
|
11
11
|
DefaultResourceLoader,
|
|
12
|
+
type InlineExtension,
|
|
12
13
|
ModelRuntime,
|
|
13
14
|
ProjectTrustStore,
|
|
14
15
|
SessionManager,
|
|
@@ -157,10 +158,10 @@ function loaderKey(cwd: string): string {
|
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
/** 每个热会话必须拥有自己的扩展 runtime;这里只创建,不加入发现缓存。 */
|
|
160
|
-
export async function createSessionResourceLoader(cwd: string): Promise<DefaultResourceLoader> {
|
|
161
|
+
export async function createSessionResourceLoader(cwd: string, extensionFactories: InlineExtension[] = []): Promise<DefaultResourceLoader> {
|
|
161
162
|
const settingsManager = getSettingsManager(cwd);
|
|
162
163
|
settingsManager.setProjectTrusted(resolveProjectTrust(cwd).trusted);
|
|
163
|
-
const loader = new DefaultResourceLoader({ cwd: path.resolve(cwd), agentDir: getAgentDir(), settingsManager });
|
|
164
|
+
const loader = new DefaultResourceLoader({ cwd: path.resolve(cwd), agentDir: getAgentDir(), settingsManager, extensionFactories });
|
|
164
165
|
const reload = loader.reload.bind(loader);
|
|
165
166
|
loader.reload = async (options) => {
|
|
166
167
|
settingsManager.setProjectTrusted(resolveProjectTrust(cwd).trusted);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
type ActivityKind = "session" | "swarm" | "tests";
|
|
2
|
+
interface Registry { active: Map<symbol, ActivityKind>; maintenance: boolean }
|
|
3
|
+
|
|
4
|
+
// Route bundles must share a process-wide gate, not separate module-local registries.
|
|
5
|
+
const globals = globalThis as typeof globalThis & { __piwebRuntimeActivity?: Registry };
|
|
6
|
+
const registry = globals.__piwebRuntimeActivity ??= { active: new Map(), maintenance: false };
|
|
7
|
+
|
|
8
|
+
export class RuntimeBusyError extends Error {}
|
|
9
|
+
|
|
10
|
+
export function beginActivity(kind: ActivityKind): () => void {
|
|
11
|
+
if (registry.maintenance) throw new RuntimeBusyError("update maintenance is active; wait for the release to finish");
|
|
12
|
+
const token = Symbol(kind);
|
|
13
|
+
registry.active.set(token, kind);
|
|
14
|
+
return () => { registry.active.delete(token); };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function beginMaintenance(): () => void {
|
|
18
|
+
if (registry.maintenance || registry.active.size) {
|
|
19
|
+
throw new RuntimeBusyError("work is active; wait for sessions, swarms and tests to finish before entering the maintenance window");
|
|
20
|
+
}
|
|
21
|
+
registry.maintenance = true;
|
|
22
|
+
let released = false;
|
|
23
|
+
return () => {
|
|
24
|
+
if (!released) registry.maintenance = false;
|
|
25
|
+
released = true;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -18,6 +18,7 @@ import { dshSource } from "./dsh";
|
|
|
18
18
|
import { zcodeSource } from "./zcode";
|
|
19
19
|
import { opencodeSource } from "./opencode";
|
|
20
20
|
import { importedKey, listImportedKeys, findImported, writeImportedSession } from "./writer";
|
|
21
|
+
import { withImportLock } from "./lock";
|
|
21
22
|
|
|
22
23
|
export const IMPORT_SOURCE_MODULES: Record<ImportSource, ImportSourceModule> = {
|
|
23
24
|
claude: claudeSource,
|
|
@@ -146,6 +147,10 @@ async function oversize(location: string): Promise<boolean> {
|
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
export async function importSessions(items: ImportSelection[], options: ImportOptions): Promise<ImportReport> {
|
|
150
|
+
return withImportLock(options.agentDir, () => importSessionsLocked(items, options));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function importSessionsLocked(items: ImportSelection[], options: ImportOptions): Promise<ImportReport> {
|
|
149
154
|
const results: ImportOutcome[] = [];
|
|
150
155
|
const workspaces = new Set<string>();
|
|
151
156
|
const fallback = (await isDirectory(options.fallbackCwd)) ? path.resolve(options.fallbackCwd as string) : undefined;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import lockfile from "proper-lockfile";
|
|
4
|
+
|
|
5
|
+
const pending = new Map<string, Promise<void>>();
|
|
6
|
+
|
|
7
|
+
export async function withImportLock<T>(agentDir: string, operation: () => Promise<T>): Promise<T> {
|
|
8
|
+
await fs.mkdir(agentDir, { recursive: true });
|
|
9
|
+
const real = await fs.realpath(agentDir);
|
|
10
|
+
const key = process.platform === "win32" ? real.toLowerCase() : real;
|
|
11
|
+
const previous = pending.get(key) ?? Promise.resolve();
|
|
12
|
+
const result = previous.then(async () => {
|
|
13
|
+
// Serialize the global provenance lookup and write, including across server processes.
|
|
14
|
+
const release = await lockfile.lock(path.join(real, "piweb-session-import"), {
|
|
15
|
+
realpath: false, stale: 120_000,
|
|
16
|
+
retries: { retries: 60, minTimeout: 100, maxTimeout: 500 },
|
|
17
|
+
});
|
|
18
|
+
try { return await operation(); }
|
|
19
|
+
finally { await release(); }
|
|
20
|
+
});
|
|
21
|
+
const settled = result.then(() => undefined, () => undefined);
|
|
22
|
+
pending.set(key, settled);
|
|
23
|
+
void settled.then(() => { if (pending.get(key) === settled) pending.delete(key); });
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
@@ -235,9 +235,14 @@ export async function writeImportedSession(session: ImportedSession, options: Wr
|
|
|
235
235
|
const dir = path.join(options.agentDir, "sessions", encodeSessionDir(options.cwd));
|
|
236
236
|
await fs.mkdir(dir, { recursive: true });
|
|
237
237
|
const target = path.join(dir, importedFileName(summary.source, summary.externalId, createdAt));
|
|
238
|
-
const temporary = `${target}.${
|
|
239
|
-
|
|
240
|
-
|
|
238
|
+
const temporary = `${target}.${randomUUID()}.tmp`;
|
|
239
|
+
try {
|
|
240
|
+
await fs.writeFile(temporary, `${lines.join("\n")}\n`, { encoding: "utf8", flag: "wx" });
|
|
241
|
+
// Publish a complete file atomically without replacing an existing imported session.
|
|
242
|
+
await fs.link(temporary, target);
|
|
243
|
+
} finally {
|
|
244
|
+
await fs.rm(temporary, { force: true });
|
|
245
|
+
}
|
|
241
246
|
return { path: target, messageCount: messages.messages.length, skipped };
|
|
242
247
|
}
|
|
243
248
|
|