@fitchmultz/pi-subagents 0.0.0-stage → 0.44.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1654 -0
- package/LICENSE +21 -0
- package/README.md +1533 -2
- package/agents/context-builder.md +97 -0
- package/agents/debugger.md +51 -0
- package/agents/delegate.md +13 -0
- package/agents/fixer.md +37 -0
- package/agents/oracle.md +71 -0
- package/agents/planner.md +72 -0
- package/agents/researcher.md +51 -0
- package/agents/reviewer-claude.md +69 -0
- package/agents/reviewer-gpt.md +68 -0
- package/agents/reviewer-ponytail.md +82 -0
- package/agents/reviewer-security.md +81 -0
- package/agents/reviewer.md +69 -0
- package/agents/scout.md +66 -0
- package/agents/ui-designer.md +51 -0
- package/agents/watcher.md +26 -0
- package/agents/worker.md +46 -0
- package/agents/writer.md +36 -0
- package/dist/agents/agent-management.js +923 -0
- package/dist/agents/agent-scope.js +7 -0
- package/dist/agents/agent-selection.js +20 -0
- package/dist/agents/agent-serializer.js +100 -0
- package/dist/agents/agents.js +752 -0
- package/dist/agents/chain-serializer.js +367 -0
- package/dist/agents/frontmatter.js +21 -0
- package/dist/agents/identity.js +29 -0
- package/dist/agents/skills.js +570 -0
- package/dist/extension/build-info.js +1 -0
- package/dist/extension/compact-tools.js +112 -0
- package/dist/extension/config.js +22 -0
- package/dist/extension/control-notices.js +29 -0
- package/dist/extension/doctor.js +143 -0
- package/dist/extension/fanout-child.js +326 -0
- package/dist/extension/index.js +577 -0
- package/dist/extension/schemas.js +416 -0
- package/dist/extension/tool-input.js +20 -0
- package/dist/extension/tool-result.js +14 -0
- package/dist/history/canonical-result.js +81 -0
- package/dist/history/database.js +72 -0
- package/dist/history/index.js +199 -0
- package/dist/history/ingest.js +468 -0
- package/dist/history/queries.js +350 -0
- package/dist/history/store.js +216 -0
- package/dist/history/types.js +4 -0
- package/dist/history/worker.js +466 -0
- package/dist/intercom/intercom-bridge.js +112 -0
- package/dist/intercom/live-intercom.js +94 -0
- package/dist/intercom/result-intercom.js +373 -0
- package/dist/pi-intercom/agent-dir.js +10 -0
- package/dist/pi-intercom/bridge.js +750 -0
- package/dist/pi-intercom/broker/broker.js +426 -0
- package/dist/pi-intercom/broker/client.js +492 -0
- package/dist/pi-intercom/broker/framing.js +65 -0
- package/dist/pi-intercom/broker/paths.js +42 -0
- package/dist/pi-intercom/broker/pid.js +66 -0
- package/dist/pi-intercom/broker/spawn.js +221 -0
- package/dist/pi-intercom/config.js +102 -0
- package/dist/pi-intercom/index.js +2942 -0
- package/dist/pi-intercom/reply-tracker.js +182 -0
- package/dist/pi-intercom/session-targets.js +128 -0
- package/dist/pi-intercom/subagent-live-events.js +132 -0
- package/dist/pi-intercom/topics.js +157 -0
- package/dist/pi-intercom/types.js +163 -0
- package/dist/pi-intercom/ui/compose.js +269 -0
- package/dist/pi-intercom/ui/inline-message.js +80 -0
- package/dist/pi-intercom/ui/session-list.js +124 -0
- package/dist/runs/background/async-control.js +95 -0
- package/dist/runs/background/async-execution.js +676 -0
- package/dist/runs/background/async-job-tracker.js +383 -0
- package/dist/runs/background/async-result-file.js +58 -0
- package/dist/runs/background/async-resume.js +305 -0
- package/dist/runs/background/async-status.js +442 -0
- package/dist/runs/background/completion-dedupe.js +69 -0
- package/dist/runs/background/completion-delivery.js +245 -0
- package/dist/runs/background/notify.js +71 -0
- package/dist/runs/background/parallel-groups.js +55 -0
- package/dist/runs/background/result-watcher.js +426 -0
- package/dist/runs/background/run-id-resolver.js +64 -0
- package/dist/runs/background/run-status.js +382 -0
- package/dist/runs/background/stale-run-reconciler.js +335 -0
- package/dist/runs/background/subagent-runner-launcher.js +55 -0
- package/dist/runs/background/subagent-runner.js +1872 -0
- package/dist/runs/background/top-level-async.js +5 -0
- package/dist/runs/foreground/chain-clarify.js +1152 -0
- package/dist/runs/foreground/clarify-invocation.js +72 -0
- package/dist/runs/foreground/execution-input.js +423 -0
- package/dist/runs/foreground/foreground-control.js +817 -0
- package/dist/runs/foreground/question-control.js +88 -0
- package/dist/runs/foreground/run-async-path.js +205 -0
- package/dist/runs/foreground/subagent-executor.js +442 -0
- package/dist/runs/foreground/subagent-params.js +165 -0
- package/dist/runs/foreground/wait-run.js +141 -0
- package/dist/runs/shared/acceptance-contract.js +253 -0
- package/dist/runs/shared/acceptance-evaluation.js +256 -0
- package/dist/runs/shared/acceptance-finalization.js +280 -0
- package/dist/runs/shared/acceptance-reports.js +147 -0
- package/dist/runs/shared/acceptance.js +4 -0
- package/dist/runs/shared/chain-outputs.js +110 -0
- package/dist/runs/shared/child-attempt.js +693 -0
- package/dist/runs/shared/child-execution-cwd.js +96 -0
- package/dist/runs/shared/child-observations.js +114 -0
- package/dist/runs/shared/claude-code.js +199 -0
- package/dist/runs/shared/completion-guard.js +31 -0
- package/dist/runs/shared/dynamic-fanout.js +274 -0
- package/dist/runs/shared/history-index.js +72 -0
- package/dist/runs/shared/mcp-direct-tool-allowlist.js +334 -0
- package/dist/runs/shared/model-fallback.js +185 -0
- package/dist/runs/shared/mutating-tool-guard.js +170 -0
- package/dist/runs/shared/native-finalization.js +101 -0
- package/dist/runs/shared/native-usage.js +153 -0
- package/dist/runs/shared/nested-events.js +700 -0
- package/dist/runs/shared/nested-path.js +48 -0
- package/dist/runs/shared/nested-render.js +108 -0
- package/dist/runs/shared/parallel-utils.js +61 -0
- package/dist/runs/shared/parent-receipts.js +68 -0
- package/dist/runs/shared/parent-usage.js +121 -0
- package/dist/runs/shared/pi-args.js +310 -0
- package/dist/runs/shared/pi-spawn.js +52 -0
- package/dist/runs/shared/run-history-queries.js +45 -0
- package/dist/runs/shared/run-history.js +110 -0
- package/dist/runs/shared/run-id-ambiguity.js +4 -0
- package/dist/runs/shared/run-records.js +679 -0
- package/dist/runs/shared/session-cwd-preload.js +33 -0
- package/dist/runs/shared/single-output.js +222 -0
- package/dist/runs/shared/streaming-text.js +15 -0
- package/dist/runs/shared/structured-output.js +55 -0
- package/dist/runs/shared/subagent-control.js +179 -0
- package/dist/runs/shared/subagent-prompt-runtime.js +197 -0
- package/dist/runs/shared/subagent-tool-loop-guard.js +66 -0
- package/dist/runs/shared/supervisor-questions.js +350 -0
- package/dist/runs/shared/workflow-graph.js +214 -0
- package/dist/runs/shared/workflow-policy.js +84 -0
- package/dist/runs/shared/worktree.js +525 -0
- package/dist/shared/agent-context-policy.js +126 -0
- package/dist/shared/artifacts.js +96 -0
- package/dist/shared/atomic-json.js +13 -0
- package/dist/shared/cooperative.js +20 -0
- package/dist/shared/execution-cwd.js +14 -0
- package/dist/shared/file-coalescer.js +21 -0
- package/dist/shared/fork-context.js +49 -0
- package/dist/shared/formatters.js +85 -0
- package/dist/shared/journal-reader.js +407 -0
- package/dist/shared/lazy-tools.js +32 -0
- package/dist/shared/model-info.js +68 -0
- package/dist/shared/native-session.js +15 -0
- package/dist/shared/native-tui.js +18 -0
- package/dist/shared/native-typebox.js +20 -0
- package/dist/shared/post-exit-stdio-guard.js +197 -0
- package/dist/shared/prompt-sections.js +8 -0
- package/dist/shared/session-entries.js +52 -0
- package/dist/shared/session-identity.js +14 -0
- package/dist/shared/settings.js +254 -0
- package/dist/shared/status-format.js +111 -0
- package/dist/shared/temp-root.js +114 -0
- package/dist/shared/types.js +207 -0
- package/dist/shared/ui-mode.js +3 -0
- package/dist/shared/utils.js +334 -0
- package/dist/slash/prompt-template-bridge.js +298 -0
- package/dist/slash/slash-bridge.js +116 -0
- package/dist/slash/slash-commands.js +607 -0
- package/dist/slash/slash-live-state.js +246 -0
- package/dist/tui/action-hints.js +75 -0
- package/dist/tui/agent-history.js +197 -0
- package/dist/tui/agent-view.js +1656 -0
- package/dist/tui/render.js +1515 -0
- package/docs/intercom-bridge.md +212 -0
- package/docs/intercom.md +581 -0
- package/docs/performance.md +121 -0
- package/package.json +102 -4
- package/prompts/gather-context-and-clarify.md +14 -0
- package/prompts/parallel-cleanup.md +62 -0
- package/prompts/parallel-context-build.md +56 -0
- package/prompts/parallel-handoff-plan.md +62 -0
- package/prompts/parallel-research.md +51 -0
- package/prompts/parallel-review.md +59 -0
- package/prompts/review-loop.md +42 -0
- package/scripts/build.mjs +134 -0
- package/scripts/check-compat.mjs +48 -0
- package/scripts/compat-host.mjs +19 -0
- package/scripts/intercom-bridge-credentials.mjs +143 -0
- package/scripts/intercom-remote.mjs +103 -0
- package/scripts/linux-smoke.sh +51 -0
- package/scripts/local-install-smoke.mjs +66 -0
- package/scripts/native-package-smoke.mjs +82 -0
- package/scripts/package-smoke.mjs +209 -0
- package/scripts/prepare.mjs +67 -0
- package/scripts/real-pi-smoke.mjs +415 -0
- package/scripts/run-tests.mjs +149 -0
- package/skills/pi-intercom/SKILL.md +113 -0
- package/skills/pi-intercom/evals/evals.json +29 -0
- package/skills/pi-intercom/evals/trigger-evals.json +47 -0
- package/skills/pi-intercom/references/peer-sessions.md +69 -0
- package/skills/pi-subagents/SKILL.md +76 -0
- package/skills/pi-subagents/evals/evals.json +29 -0
- package/skills/pi-subagents/evals/trigger-evals.json +42 -0
- package/skills/pi-subagents/references/full-orchestration-guide.md +863 -0
- package/src/agents/agent-management.ts +813 -0
- package/src/agents/agent-scope.ts +7 -0
- package/src/agents/agent-selection.ts +23 -0
- package/src/agents/agent-serializer.ts +99 -0
- package/src/agents/agents.ts +916 -0
- package/src/agents/chain-serializer.ts +329 -0
- package/src/agents/frontmatter.ts +21 -0
- package/src/agents/identity.ts +30 -0
- package/src/agents/skills.ts +637 -0
- package/src/extension/build-info.ts +2 -0
- package/src/extension/compact-tools.ts +114 -0
- package/src/extension/config.ts +23 -0
- package/src/extension/control-notices.ts +47 -0
- package/src/extension/doctor.ts +171 -0
- package/src/extension/fanout-child.ts +312 -0
- package/src/extension/index.ts +649 -0
- package/src/extension/schemas.ts +444 -0
- package/src/extension/tool-input.ts +19 -0
- package/src/extension/tool-result.ts +16 -0
- package/src/history/canonical-result.ts +62 -0
- package/src/history/database.ts +59 -0
- package/src/history/index.ts +156 -0
- package/src/history/ingest.ts +349 -0
- package/src/history/queries.ts +241 -0
- package/src/history/store.ts +203 -0
- package/src/history/types.ts +166 -0
- package/src/history/worker.ts +321 -0
- package/src/intercom/intercom-bridge.ts +123 -0
- package/src/intercom/live-intercom.ts +104 -0
- package/src/intercom/result-intercom.ts +429 -0
- package/src/pi-intercom/agent-dir.ts +9 -0
- package/src/pi-intercom/bridge.ts +668 -0
- package/src/pi-intercom/broker/broker.ts +463 -0
- package/src/pi-intercom/broker/client.ts +558 -0
- package/src/pi-intercom/broker/framing.ts +77 -0
- package/src/pi-intercom/broker/paths.ts +47 -0
- package/src/pi-intercom/broker/pid.ts +59 -0
- package/src/pi-intercom/broker/spawn.ts +248 -0
- package/src/pi-intercom/config.ts +142 -0
- package/src/pi-intercom/index.ts +2929 -0
- package/src/pi-intercom/reply-tracker.ts +211 -0
- package/src/pi-intercom/session-targets.ts +172 -0
- package/src/pi-intercom/subagent-live-events.ts +133 -0
- package/src/pi-intercom/topics.ts +125 -0
- package/src/pi-intercom/types.ts +271 -0
- package/src/pi-intercom/ui/compose.ts +289 -0
- package/src/pi-intercom/ui/inline-message.ts +92 -0
- package/src/pi-intercom/ui/session-list.ts +139 -0
- package/src/runs/background/async-control.ts +89 -0
- package/src/runs/background/async-execution.ts +850 -0
- package/src/runs/background/async-job-tracker.ts +385 -0
- package/src/runs/background/async-result-file.ts +62 -0
- package/src/runs/background/async-resume.ts +333 -0
- package/src/runs/background/async-status.ts +481 -0
- package/src/runs/background/completion-dedupe.ts +79 -0
- package/src/runs/background/completion-delivery.ts +195 -0
- package/src/runs/background/notify.ts +123 -0
- package/src/runs/background/parallel-groups.ts +53 -0
- package/src/runs/background/result-watcher.ts +423 -0
- package/src/runs/background/run-id-resolver.ts +69 -0
- package/src/runs/background/run-status.ts +384 -0
- package/src/runs/background/stale-run-reconciler.ts +369 -0
- package/src/runs/background/subagent-runner-launcher.ts +56 -0
- package/src/runs/background/subagent-runner.ts +2053 -0
- package/src/runs/background/top-level-async.ts +13 -0
- package/src/runs/foreground/chain-clarify.ts +1340 -0
- package/src/runs/foreground/clarify-invocation.ts +77 -0
- package/src/runs/foreground/execution-input.ts +467 -0
- package/src/runs/foreground/foreground-control.ts +821 -0
- package/src/runs/foreground/question-control.ts +82 -0
- package/src/runs/foreground/run-async-path.ts +243 -0
- package/src/runs/foreground/subagent-executor.ts +501 -0
- package/src/runs/foreground/subagent-params.ts +320 -0
- package/src/runs/foreground/wait-run.ts +124 -0
- package/src/runs/shared/acceptance-contract.ts +257 -0
- package/src/runs/shared/acceptance-evaluation.ts +275 -0
- package/src/runs/shared/acceptance-finalization.ts +353 -0
- package/src/runs/shared/acceptance-reports.ts +152 -0
- package/src/runs/shared/acceptance.ts +33 -0
- package/src/runs/shared/chain-outputs.ts +114 -0
- package/src/runs/shared/child-attempt.ts +623 -0
- package/src/runs/shared/child-execution-cwd.ts +91 -0
- package/src/runs/shared/child-observations.ts +99 -0
- package/src/runs/shared/claude-code.ts +243 -0
- package/src/runs/shared/completion-guard.ts +35 -0
- package/src/runs/shared/dynamic-fanout.ts +295 -0
- package/src/runs/shared/history-index.ts +69 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +376 -0
- package/src/runs/shared/model-fallback.ts +219 -0
- package/src/runs/shared/mutating-tool-guard.ts +194 -0
- package/src/runs/shared/native-finalization.ts +128 -0
- package/src/runs/shared/native-usage.ts +113 -0
- package/src/runs/shared/nested-events.ts +752 -0
- package/src/runs/shared/nested-path.ts +52 -0
- package/src/runs/shared/nested-render.ts +119 -0
- package/src/runs/shared/parallel-utils.ts +153 -0
- package/src/runs/shared/parent-receipts.ts +59 -0
- package/src/runs/shared/parent-usage.ts +121 -0
- package/src/runs/shared/pi-args.ts +350 -0
- package/src/runs/shared/pi-spawn.ts +52 -0
- package/src/runs/shared/run-history-queries.ts +44 -0
- package/src/runs/shared/run-history.ts +100 -0
- package/src/runs/shared/run-id-ambiguity.ts +4 -0
- package/src/runs/shared/run-records.ts +607 -0
- package/src/runs/shared/session-cwd-preload.ts +27 -0
- package/src/runs/shared/single-output.ts +267 -0
- package/src/runs/shared/streaming-text.ts +18 -0
- package/src/runs/shared/structured-output.ts +68 -0
- package/src/runs/shared/subagent-control.ts +219 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +201 -0
- package/src/runs/shared/subagent-tool-loop-guard.ts +89 -0
- package/src/runs/shared/supervisor-questions.ts +415 -0
- package/src/runs/shared/workflow-graph.ts +220 -0
- package/src/runs/shared/workflow-policy.ts +125 -0
- package/src/runs/shared/worktree.ts +635 -0
- package/src/shared/agent-context-policy.ts +184 -0
- package/src/shared/artifacts.ts +95 -0
- package/src/shared/atomic-json.ts +16 -0
- package/src/shared/cooperative.ts +21 -0
- package/src/shared/execution-cwd.ts +14 -0
- package/src/shared/file-coalescer.ts +29 -0
- package/src/shared/fork-context.ts +68 -0
- package/src/shared/formatters.ts +84 -0
- package/src/shared/journal-reader.ts +333 -0
- package/src/shared/lazy-tools.ts +36 -0
- package/src/shared/model-info.ts +81 -0
- package/src/shared/native-session.ts +8 -0
- package/src/shared/native-tui.ts +10 -0
- package/src/shared/native-typebox.ts +14 -0
- package/src/shared/post-exit-stdio-guard.ts +204 -0
- package/src/shared/prompt-sections.ts +10 -0
- package/src/shared/session-entries.ts +49 -0
- package/src/shared/session-identity.ts +18 -0
- package/src/shared/settings.ts +384 -0
- package/src/shared/status-format.ts +123 -0
- package/src/shared/temp-root.ts +107 -0
- package/src/shared/types.ts +1260 -0
- package/src/shared/ui-mode.ts +5 -0
- package/src/shared/utils.ts +342 -0
- package/src/slash/prompt-template-bridge.ts +408 -0
- package/src/slash/slash-bridge.ts +178 -0
- package/src/slash/slash-commands.ts +617 -0
- package/src/slash/slash-live-state.ts +292 -0
- package/src/tui/action-hints.ts +69 -0
- package/src/tui/agent-history.ts +210 -0
- package/src/tui/agent-view.ts +1323 -0
- package/src/tui/render.ts +1550 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { ownedRunView } from "../runs/shared/run-records.ts";
|
|
4
|
+
import { acceptanceHumanAction } from "../runs/shared/acceptance-evaluation.ts";
|
|
5
|
+
import { getRunMetadataDir, listRunQuestions, summarizeSupervisorQuestion } from "../runs/shared/supervisor-questions.ts";
|
|
6
|
+
import { HistoryIndexError } from "./types.ts";
|
|
7
|
+
import type { ForegroundResumeRun, HistoryOwner, HistoryRunRow, OwnedRun, Request, Response } from "./types.ts";
|
|
8
|
+
import type { SubagentState } from "../shared/types.ts";
|
|
9
|
+
import { exactTextDigest, HistoryStore, safeText } from "./store.ts";
|
|
10
|
+
import { readCanonicalOutput, readSavedOutput } from "./canonical-result.ts";
|
|
11
|
+
import { SourceIngest, identity, stamp } from "./ingest.ts";
|
|
12
|
+
import { HistoryQueries } from "./queries.ts";
|
|
13
|
+
|
|
14
|
+
const agentDir = process.argv[2];
|
|
15
|
+
process.umask(0o077);
|
|
16
|
+
let store: HistoryStore | undefined, queries: HistoryQueries | undefined, owner: HistoryOwner | undefined;
|
|
17
|
+
const runs = new Map<string, OwnedRun>(), foreground = new Map<string, ForegroundResumeRun>();
|
|
18
|
+
const dirtyRuns = new Set<string>(), dirtySources = new Map<string, boolean>();
|
|
19
|
+
const watchers = new Map<string, fs.FSWatcher>();
|
|
20
|
+
const sourceWatchers = new Map<string, fs.FSWatcher>();
|
|
21
|
+
const watchIdentities = new WeakMap<fs.FSWatcher, string>();
|
|
22
|
+
const watchedSources = new Map<string, Map<string, string>>();
|
|
23
|
+
let job: SourceIngest | undefined, scheduled = false, closed = false;
|
|
24
|
+
let retryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
25
|
+
let runErrors = new Set<string>();
|
|
26
|
+
const barriers: Array<{ id: number; runId?: string }> = [];
|
|
27
|
+
const controlQueries: number[] = [];
|
|
28
|
+
function replyControls(id: number): void {
|
|
29
|
+
send({ id, value: Boolean(store!.get("SELECT id FROM runs WHERE attention<4 OR state='live' LIMIT 1")) });
|
|
30
|
+
}
|
|
31
|
+
function send(response: Response): void {
|
|
32
|
+
// The parent can disconnect after process.connected was read; then this and later replies are undeliverable, so stop as on disconnect.
|
|
33
|
+
if (process.connected) process.send!(response, undefined, undefined, (error) => { if (error) shutdown(); });
|
|
34
|
+
}
|
|
35
|
+
function changed(): void { send({ changed: true }); }
|
|
36
|
+
function errorReply(id: number, error: unknown): void {
|
|
37
|
+
const code = error instanceof HistoryIndexError ? error.code : "UNAVAILABLE";
|
|
38
|
+
// Parser errors can include fragments from the malformed body. IPC/logs must never carry them.
|
|
39
|
+
send({ id, error: { code, message: error instanceof HistoryIndexError ? error.message : "History index operation failed; authoritative files were not substituted for query results." } });
|
|
40
|
+
}
|
|
41
|
+
function unknown(run: OwnedRun, diagnosis = "Run summary is awaiting background canonical projection."): HistoryRunRow {
|
|
42
|
+
return { ...run, state: "unknown", updatedAt: run.startedAt, attention: ["unknown"], canInterrupt: false, continuations: [],
|
|
43
|
+
children: run.children.map((child) => ({ ...child, state: "unknown", configuration: "legacy-partial" })), diagnosis };
|
|
44
|
+
}
|
|
45
|
+
function compactView(view: HistoryRunRow): HistoryRunRow {
|
|
46
|
+
return {
|
|
47
|
+
...view, task: safeText(view.task, 2048), error: view.error && safeText(view.error), diagnosis: view.diagnosis && safeText(view.diagnosis), recoveryError: view.recoveryError && safeText(view.recoveryError),
|
|
48
|
+
review: view.review && { ...view.review, message: view.review.message && safeText(view.review.message) }, continuations: [],
|
|
49
|
+
questions: view.questions?.map((question) => {
|
|
50
|
+
const summary = summarizeSupervisorQuestion(question);
|
|
51
|
+
return { ...summary, message: safeText(question.message, 2048), answer: summary.answer && { ...summary.answer, message: safeText(question.answer?.message, 2048) } };
|
|
52
|
+
}),
|
|
53
|
+
children: view.children.map((child) => ({
|
|
54
|
+
agent: child.agent, index: child.index, workflowNodeId: child.workflowNodeId, sessionFile: child.sessionFile, task: child.task && safeText(child.task), label: child.label && safeText(child.label, 256),
|
|
55
|
+
state: child.state, configuration: child.configuration, missingSession: child.missingSession, identityUnavailable: child.identityUnavailable, modelSelection: child.modelSelection,
|
|
56
|
+
savedConfiguration: child.launch && { model: child.launch.model, thinking: child.launch.thinking, modelRecordedAt: child.launch.modelRecordedAt },
|
|
57
|
+
humanAction: child.result?.acceptance && safeText(acceptanceHumanAction(child.result.acceptance), 2048), acceptanceStatus: child.result?.acceptance?.status,
|
|
58
|
+
activity: child.activity && { status: child.activity.status, currentTool: child.activity.currentTool, currentToolArgs: child.activity.currentToolArgs && safeText(child.activity.currentToolArgs, 256), currentPath: child.activity.currentPath,
|
|
59
|
+
streamingText: child.activity.streamingText && safeText(child.activity.streamingText.slice(-2048), 2048), lastActivityAt: child.activity.lastActivityAt, recentOutput: child.activity.recentOutput?.slice(-4).map((line) => safeText(line, 256)) },
|
|
60
|
+
result: child.result && { agent: child.result.agent, task: safeText(child.result.task), exitCode: child.result.exitCode,
|
|
61
|
+
usage: child.result.usage && { input: child.result.usage.input, output: child.result.usage.output, cacheRead: child.result.usage.cacheRead, cacheWrite: child.result.usage.cacheWrite, cost: child.result.usage.cost, turns: child.result.usage.turns },
|
|
62
|
+
finalOutput: safeText(child.result.finalOutput, 1024), error: child.result.error && safeText(child.result.error), sessionFile: child.result.sessionFile, terminalEntryId: child.result.terminalEntryId, terminalLeafId: child.result.terminalLeafId,
|
|
63
|
+
detached: child.result.detached, interrupted: child.result.interrupted, timedOut: child.result.timedOut, nativeSessionId: child.result.nativeSessionId, accounting: child.result.accounting,
|
|
64
|
+
agentProcessExit: child.result.agentProcessExit, model: child.result.model, fullOutputPath: child.result.fullOutputPath },
|
|
65
|
+
})), summary: safeText(view.children.map((child) => child.result?.finalOutput ?? child.result?.error ?? "").filter(Boolean).join(" | "), 1024),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function projection(run: OwnedRun): HistoryRunRow {
|
|
69
|
+
const state: SubagentState = {
|
|
70
|
+
baseCwd: run.cwd, currentSessionId: owner!.ownerSessionId, ownedRuns: runs, foregroundRuns: foreground,
|
|
71
|
+
asyncJobs: new Map(), cleanupTimers: new Map(), lastUiContext: null, poller: null, completionSeen: new Map(), watcher: null, watcherRestartTimer: null,
|
|
72
|
+
resultFileCoalescer: { schedule: () => false, clear() {} },
|
|
73
|
+
};
|
|
74
|
+
const questions = listRunQuestions(getRunMetadataDir(run.runId)).filter((question) => question.ownerSessionId === owner!.ownerSessionId && ["awaiting_input", "answer_pending"].includes(question.state));
|
|
75
|
+
const options = { pendingInput: questions.length > 0, includeContinuations: false, readConfiguration: false as const, reconcile: false };
|
|
76
|
+
return { ...ownedRunView(run, state, options), questions };
|
|
77
|
+
}
|
|
78
|
+
function watch(directory: string, recursive: boolean, listener: (name: string | null) => void, handles = watchers): void {
|
|
79
|
+
const existing = handles.get(directory);
|
|
80
|
+
try {
|
|
81
|
+
const physical = identity(fs.statSync(directory, { bigint: true }));
|
|
82
|
+
if (existing && watchIdentities.get(existing) === physical) return;
|
|
83
|
+
const watcher = fs.watch(directory, { recursive }, (event, name) => {
|
|
84
|
+
const filename = name?.toString() ?? null;
|
|
85
|
+
if (event === "rename" && filename === path.basename(directory) && handles.get(directory) === watcher) {
|
|
86
|
+
watchIdentities.delete(watcher);
|
|
87
|
+
watch(directory, recursive, listener, handles);
|
|
88
|
+
listener(null);
|
|
89
|
+
} else listener(filename);
|
|
90
|
+
schedule();
|
|
91
|
+
});
|
|
92
|
+
watchIdentities.set(watcher, physical);
|
|
93
|
+
watcher.on("error", () => { watcher.close(); if (handles.get(directory) === watcher) handles.delete(directory); });
|
|
94
|
+
watcher.on("close", () => { if (handles.get(directory) === watcher) handles.delete(directory); });
|
|
95
|
+
handles.set(directory, watcher);
|
|
96
|
+
// Native close fences macOS stream re-registration; admit the replacement first.
|
|
97
|
+
existing?.close();
|
|
98
|
+
} catch {
|
|
99
|
+
if (existing && handles.get(directory) === existing) watchIdentities.delete(existing);
|
|
100
|
+
/* Known-handle background census recovers absent directories and dropped watches. */
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function resetDirectoryWatch(directory: string): void {
|
|
104
|
+
for (const handles of [watchers, sourceWatchers]) {
|
|
105
|
+
const watcher = handles.get(directory);
|
|
106
|
+
if (watcher) watchIdentities.delete(watcher);
|
|
107
|
+
}
|
|
108
|
+
for (const id of watchedSources.get(directory)?.keys() ?? []) dirtySources.set(id, dirtySources.get(id) ?? false);
|
|
109
|
+
}
|
|
110
|
+
function watchSources(directory: string): void {
|
|
111
|
+
if (!watchedSources.has(directory)) return;
|
|
112
|
+
watch(directory, false, (name) => {
|
|
113
|
+
for (const [id, filename] of watchedSources.get(directory) ?? []) if (!name || filename === name) {
|
|
114
|
+
if (!dirtySources.has(id) && job?.id !== id) {
|
|
115
|
+
try {
|
|
116
|
+
const source = store?.source(id);
|
|
117
|
+
if (source?.stamp && ["current", "partial", "degraded"].includes(source.state)
|
|
118
|
+
&& source.stamp === stamp(fs.statSync(source.path, { bigint: true }))) continue;
|
|
119
|
+
} catch { /* Missing or unreadable sources still need reconciliation. */ }
|
|
120
|
+
}
|
|
121
|
+
dirtySources.set(id, dirtySources.get(id) ?? false);
|
|
122
|
+
}
|
|
123
|
+
}, sourceWatchers);
|
|
124
|
+
}
|
|
125
|
+
function installWatches(runId?: string): void {
|
|
126
|
+
if (!owner || !store) return;
|
|
127
|
+
const root = path.join(agentDir, "sessions", "subagent-runs");
|
|
128
|
+
// Linux emulates recursive watching by scanning directories on creation.
|
|
129
|
+
// Only admitted run trees may incur that work, never the global foreign tree.
|
|
130
|
+
watch(root, false, (name) => {
|
|
131
|
+
const runId = name;
|
|
132
|
+
for (const id of runId ? runs.has(runId) ? [runId] : [] : runs.keys()) {
|
|
133
|
+
const directory = path.join(root, id);
|
|
134
|
+
resetDirectoryWatch(directory);
|
|
135
|
+
dirtyRuns.add(id);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
for (const id of runId ? [runId] : runs.keys()) {
|
|
139
|
+
const directory = path.join(root, id);
|
|
140
|
+
watch(directory, true, (name) => {
|
|
141
|
+
if (!name) resetDirectoryWatch(directory);
|
|
142
|
+
dirtyRuns.add(id);
|
|
143
|
+
});
|
|
144
|
+
watchSources(directory);
|
|
145
|
+
}
|
|
146
|
+
for (const source of store.all(`SELECT id,path FROM sources WHERE id IN (SELECT source_id FROM children${runId ? " WHERE run_id=?" : ""})`, ...(runId ? [runId] : []))) {
|
|
147
|
+
const directory = path.dirname(source.path);
|
|
148
|
+
let linked = watchedSources.get(directory);
|
|
149
|
+
if (!linked) watchedSources.set(directory, linked = new Map());
|
|
150
|
+
linked.set(source.id, path.basename(source.path));
|
|
151
|
+
watchSources(directory);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function prune(previousSources?: string[]): void {
|
|
155
|
+
if (!store) return;
|
|
156
|
+
if (previousSources && !previousSources.length) return;
|
|
157
|
+
store.transaction(() => {
|
|
158
|
+
let removed = false;
|
|
159
|
+
if (!previousSources) for (const row of store!.all("SELECT id FROM runs")) if (!runs.has(row.id)) { store!.run("DELETE FROM runs WHERE id=?", row.id); removed = true; }
|
|
160
|
+
for (const source of store!.all(`SELECT id,path FROM sources WHERE id NOT IN (SELECT source_id FROM children WHERE source_id IS NOT NULL)${previousSources ? ` AND id IN (${previousSources.map(() => "?").join(",")})` : ""}`, ...(previousSources ?? []))) {
|
|
161
|
+
if (job && job.id === source.id) { job.close(); job = undefined; }
|
|
162
|
+
dirtySources.delete(source.id);
|
|
163
|
+
watchedSources.get(path.dirname(source.path))?.delete(source.id);
|
|
164
|
+
store!.deleteDocuments(source.id);
|
|
165
|
+
store!.run("DELETE FROM entries WHERE source_id=?", source.id); store!.run("DELETE FROM sources WHERE id=?", source.id); removed = true;
|
|
166
|
+
}
|
|
167
|
+
if (removed) store!.bump();
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function admit(run: OwnedRun): void {
|
|
171
|
+
if (!owner || run.ownerSessionId !== owner.ownerSessionId) throw new HistoryIndexError("OWNERSHIP", "Run is not owned by this session.");
|
|
172
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(run.runId) || !Array.isArray(run.children) || run.children.some((child) => !Number.isSafeInteger(child.index) || child.index < 0) || new Set(run.children.map((child) => child.index)).size !== run.children.length) throw new HistoryIndexError("INVALID", "Invalid run or child identity.");
|
|
173
|
+
if (!Number.isSafeInteger(run.startedAt) || run.startedAt < 0) throw new HistoryIndexError("INVALID", "Invalid run timestamp.");
|
|
174
|
+
runs.set(run.runId, run); dirtyRuns.add(run.runId);
|
|
175
|
+
if (!store!.get("SELECT id FROM runs WHERE id=?", run.runId)) { const view = unknown(run); store!.putView(compactView(view), view); }
|
|
176
|
+
}
|
|
177
|
+
function hasPending(runId?: string): boolean {
|
|
178
|
+
if (!runId) return Boolean(dirtyRuns.size || dirtySources.size || job);
|
|
179
|
+
if (dirtyRuns.has(runId)) return true;
|
|
180
|
+
const ids = store!.all("SELECT source_id FROM children WHERE run_id=?", runId).map((row) => row.source_id);
|
|
181
|
+
return ids.some((id) => dirtySources.has(id) || job?.id === id);
|
|
182
|
+
}
|
|
183
|
+
function finishBarriers(): void {
|
|
184
|
+
if (!dirtyRuns.size) for (const id of controlQueries.splice(0)) replyControls(id);
|
|
185
|
+
for (let index = barriers.length - 1; index >= 0; index--) {
|
|
186
|
+
const barrier = barriers[index];
|
|
187
|
+
if (hasPending(barrier.runId)) continue;
|
|
188
|
+
barriers.splice(index, 1);
|
|
189
|
+
const failures = barrier.runId ? store!.get("SELECT COUNT(*) AS count FROM sources s JOIN children c ON c.source_id=s.id WHERE c.run_id=? AND s.state IN ('error','missing','degraded')", barrier.runId).count : store!.get("SELECT COUNT(*) AS count FROM sources WHERE state IN ('error','missing','degraded')").count;
|
|
190
|
+
if (failures || (barrier.runId ? runErrors.has(barrier.runId) : runErrors.size)) errorReply(barrier.id, new HistoryIndexError("DEGRADED", "Refresh completed with missing, malformed, or unavailable owned sources; inspect page freshness."));
|
|
191
|
+
else send({ id: barrier.id });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function schedule(): void {
|
|
195
|
+
if (scheduled || closed || retryTimer) return;
|
|
196
|
+
scheduled = true; setImmediate(pump);
|
|
197
|
+
}
|
|
198
|
+
function pump(): void {
|
|
199
|
+
scheduled = false;
|
|
200
|
+
if (closed || !store) return;
|
|
201
|
+
try {
|
|
202
|
+
if (dirtyRuns.size) {
|
|
203
|
+
const id = dirtyRuns.values().next().value!; dirtyRuns.delete(id);
|
|
204
|
+
installWatches(id);
|
|
205
|
+
const previousSources = store.all("SELECT source_id FROM children WHERE run_id=? AND source_id IS NOT NULL", id).map((child) => child.source_id);
|
|
206
|
+
try { store.operations.runProjections++; const view = projection(runs.get(id)!); store.putView(compactView(view), view); runErrors.delete(id); }
|
|
207
|
+
catch { const view = unknown(runs.get(id)!, "Canonical owner summary is unavailable. Completion remains unconfirmed."); store.putView(compactView(view), view); runErrors.add(id); }
|
|
208
|
+
prune(previousSources);
|
|
209
|
+
for (const child of store.all("SELECT source_id FROM children WHERE run_id=? AND source_id IS NOT NULL", id)) if (child.source_id !== job?.id) dirtySources.set(child.source_id, dirtySources.get(child.source_id) ?? false);
|
|
210
|
+
installWatches(id); changed();
|
|
211
|
+
} else if (job) {
|
|
212
|
+
if (job.step()) {
|
|
213
|
+
const source = store.source(job.id);
|
|
214
|
+
if (source) watchSources(path.dirname(source.path));
|
|
215
|
+
job = undefined; changed();
|
|
216
|
+
}
|
|
217
|
+
} else if (dirtySources.size) {
|
|
218
|
+
const [id, force] = dirtySources.entries().next().value!; dirtySources.delete(id);
|
|
219
|
+
const source = store.source(id);
|
|
220
|
+
if (source) { activeSource = id; job = new SourceIngest(store, source, force); }
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
const id = job?.id ?? activeSource;
|
|
224
|
+
job?.close(); job = undefined;
|
|
225
|
+
if (id && store.source(id)) {
|
|
226
|
+
if (error instanceof HistoryIndexError && error.code === "INDEX_BUSY") {
|
|
227
|
+
dirtySources.set(id, true);
|
|
228
|
+
retryTimer = setTimeout(() => { retryTimer = undefined; schedule(); }, 50);
|
|
229
|
+
} else if (error instanceof HistoryIndexError && error.code === "SOURCE_CHANGED") dirtySources.set(id, true);
|
|
230
|
+
else if ((error as NodeJS.ErrnoException).code === "ENOENT") store.resetSource(id, "missing", "Linked native conversation is missing.");
|
|
231
|
+
else store.run("UPDATE sources SET state='error',error=?,checked_at=? WHERE id=?", "Linked native conversation could not be indexed.", Date.now(), id);
|
|
232
|
+
}
|
|
233
|
+
if (!(error instanceof HistoryIndexError && error.code === "INDEX_BUSY")) changed();
|
|
234
|
+
}
|
|
235
|
+
activeSource = undefined;
|
|
236
|
+
finishBarriers();
|
|
237
|
+
if (hasPending()) schedule();
|
|
238
|
+
}
|
|
239
|
+
let activeSource: string | undefined;
|
|
240
|
+
function census(): void {
|
|
241
|
+
if (!owner || !store) return;
|
|
242
|
+
for (const id of runs.keys()) dirtyRuns.add(id);
|
|
243
|
+
for (const source of store.all("SELECT id FROM sources WHERE id IN (SELECT source_id FROM children)")) dirtySources.set(source.id, false);
|
|
244
|
+
installWatches(); schedule();
|
|
245
|
+
}
|
|
246
|
+
const timer = setInterval(census, 30_000); timer.unref();
|
|
247
|
+
function clear(): void {
|
|
248
|
+
clearTimeout(retryTimer); retryTimer = undefined;
|
|
249
|
+
job?.close(); job = undefined;
|
|
250
|
+
for (const watcher of watchers.values()) watcher.close(); watchers.clear(); watchedSources.clear();
|
|
251
|
+
for (const watcher of sourceWatchers.values()) watcher.close(); sourceWatchers.clear();
|
|
252
|
+
dirtyRuns.clear(); dirtySources.clear(); runs.clear(); foreground.clear(); runErrors = new Set();
|
|
253
|
+
for (const id of controlQueries.splice(0)) errorReply(id, new HistoryIndexError("OWNER_CHANGED", "History owner changed during control discovery."));
|
|
254
|
+
for (const barrier of barriers.splice(0)) errorReply(barrier.id, new HistoryIndexError("OWNER_CHANGED", "History owner changed during refresh."));
|
|
255
|
+
store?.close(); store = undefined; queries = undefined;
|
|
256
|
+
}
|
|
257
|
+
async function handle(request: Request): Promise<void> {
|
|
258
|
+
try {
|
|
259
|
+
const input = request.input;
|
|
260
|
+
if (request.method === "setOwner") {
|
|
261
|
+
if (!input?.ownerSessionId || input.runs?.some((run: OwnedRun) => run.ownerSessionId !== input.ownerSessionId)) throw new HistoryIndexError("OWNERSHIP", "Invalid ownership snapshot.");
|
|
262
|
+
clear(); owner = input;
|
|
263
|
+
fs.mkdirSync(agentDir, { recursive: true, mode: 0o700 });
|
|
264
|
+
store = new HistoryStore(agentDir, owner!.ownerSessionId);
|
|
265
|
+
for (const run of owner!.runs) admit(run);
|
|
266
|
+
for (const fg of owner!.foregroundRuns ?? []) { if (!runs.has(fg.runId)) throw new HistoryIndexError("OWNERSHIP", "Foreground snapshot is not an admitted owned run."); foreground.set(fg.runId, fg); }
|
|
267
|
+
prune();
|
|
268
|
+
queries = new HistoryQueries(store, () => {
|
|
269
|
+
const info = store!.info(dirtyRuns.size + dirtySources.size + (job ? 1 : 0));
|
|
270
|
+
info.freshness.errors += runErrors.size;
|
|
271
|
+
if (info.freshness.errors && !info.freshness.pending) info.freshness.state = "degraded";
|
|
272
|
+
return info;
|
|
273
|
+
});
|
|
274
|
+
for (const source of store.all("SELECT id FROM sources WHERE id IN (SELECT source_id FROM children)")) dirtySources.set(source.id, true);
|
|
275
|
+
installWatches(); schedule(); send({ id: request.id }); return;
|
|
276
|
+
}
|
|
277
|
+
if (!store || !queries) throw new HistoryIndexError("NO_OWNER", "Set genuine restored ownership first.");
|
|
278
|
+
switch (request.method) {
|
|
279
|
+
case "updateRun":
|
|
280
|
+
admit(input.run);
|
|
281
|
+
if (input.foreground) foreground.set(input.run.runId, input.foreground);
|
|
282
|
+
schedule(); send({ id: request.id }); break;
|
|
283
|
+
case "refresh": {
|
|
284
|
+
if (input.runId && !runs.has(input.runId)) throw new HistoryIndexError("OWNERSHIP", "Run is not admitted by this owner.");
|
|
285
|
+
for (const id of input.runId ? [input.runId] : runs.keys()) dirtyRuns.add(id);
|
|
286
|
+
for (const child of store.all(`SELECT DISTINCT source_id FROM children WHERE source_id IS NOT NULL${input.runId ? " AND run_id=?" : ""}`, ...(input.runId ? [input.runId] : []))) dirtySources.set(child.source_id, true);
|
|
287
|
+
barriers.push({ id: request.id, runId: input.runId }); schedule(); break;
|
|
288
|
+
}
|
|
289
|
+
case "status": send({ id: request.id, value: { ...store.info(dirtyRuns.size + dirtySources.size + (job ? 1 : 0)), databaseFile: store.file,
|
|
290
|
+
physicalSources: Number(store.get("SELECT COUNT(*) AS count FROM sources").count), publishedEntries: Number(store.get("SELECT COUNT(*) AS count FROM entries WHERE published=1").count), operations: { ...store.operations } } }); break;
|
|
291
|
+
case "needsControls":
|
|
292
|
+
if (dirtyRuns.size) { controlQueries.push(request.id); schedule(); }
|
|
293
|
+
else replyControls(request.id);
|
|
294
|
+
break;
|
|
295
|
+
case "listRuns": store.operations.queries++; send({ id: request.id, value: queries.listRuns(input) }); break;
|
|
296
|
+
case "historyPage": store.operations.queries++; send({ id: request.id, value: queries.historyPage(input) }); break;
|
|
297
|
+
case "search": store.operations.queries++; send({ id: request.id, value: queries.search(input) }); break;
|
|
298
|
+
case "result": {
|
|
299
|
+
const run = runs.get(input.runId);
|
|
300
|
+
if (!run) throw new HistoryIndexError("OWNERSHIP", "Run is not admitted by this owner.");
|
|
301
|
+
if (!Number.isSafeInteger(input.index) || input.index < 0) throw new HistoryIndexError("INVALID", "Invalid child index.");
|
|
302
|
+
const view = projection(run), child = view.children.find((candidate) => candidate.index === input.index);
|
|
303
|
+
if (!child) throw new HistoryIndexError("OWNERSHIP", "Child is not admitted by this owner.");
|
|
304
|
+
if (!child.result) { send({ id: request.id, value: null }); break; }
|
|
305
|
+
const file = view.resultPath ?? path.join(getRunMetadataDir(run.runId), "contracts", `${input.index}.json`);
|
|
306
|
+
const text = child.result.fullOutputPath ? readSavedOutput(child.result.fullOutputPath)
|
|
307
|
+
: fs.existsSync(file) ? readCanonicalOutput(file, input.index) ?? child.result.finalOutput ?? "" : child.result.finalOutput ?? "";
|
|
308
|
+
if (Buffer.byteLength(text) > 16 * 1024 * 1024) throw new HistoryIndexError("RECORD_TOO_LARGE", "Selected canonical output exceeds the 16 MiB detail budget.");
|
|
309
|
+
const boundary = child.state === "live" ? {} : { terminalEntryId: child.result.terminalEntryId, endedAt: child.result.terminalEntryId ? undefined : view.updatedAt };
|
|
310
|
+
const finalResultId = queries.finalResultId({ runId: run.runId, index: input.index, ...boundary }, exactTextDigest(text));
|
|
311
|
+
send({ id: request.id, value: { text, timestamp: view.updatedAt, finalResultId } }); break;
|
|
312
|
+
}
|
|
313
|
+
case "entry": send({ id: request.id, value: queries.selected(input, false) }); break;
|
|
314
|
+
case "record": send({ id: request.id, value: queries.selected(input, true) }); break;
|
|
315
|
+
default: throw new HistoryIndexError("INVALID", "Unknown history operation.");
|
|
316
|
+
}
|
|
317
|
+
} catch (error) { errorReply(request.id, error); }
|
|
318
|
+
}
|
|
319
|
+
process.on("message", (request: Request) => { void handle(request); });
|
|
320
|
+
function shutdown(): void { closed = true; clearInterval(timer); clear(); process.exit(0); }
|
|
321
|
+
process.on("disconnect", shutdown);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import type { AgentConfig } from "../agents/agents.ts";
|
|
4
|
+
import {
|
|
5
|
+
SUBAGENT_INTERCOM_IDENTITY_REQUEST_EVENT,
|
|
6
|
+
SUBAGENT_INTERCOM_IDENTITY_RESPONSE_EVENT,
|
|
7
|
+
type IntercomEventBus,
|
|
8
|
+
} from "../shared/types.ts";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_INTERCOM_TARGET_PREFIX = "subagent-chat";
|
|
11
|
+
// Resolve the bundled intercom extension next to this module in both layouts:
|
|
12
|
+
// TypeScript sources (tests, jiti) and compiled dist output.
|
|
13
|
+
const BUNDLED_INTERCOM_EXTENSION_PATH = fileURLToPath(
|
|
14
|
+
new URL(import.meta.url.endsWith(".ts") ? "../pi-intercom/index.ts" : "../pi-intercom/index.js", import.meta.url),
|
|
15
|
+
);
|
|
16
|
+
export const INTERCOM_BRIDGE_MARKER = "Intercom orchestration channel:";
|
|
17
|
+
const DEFAULT_INTERCOM_BRIDGE_TEMPLATE = `The inherited thread is reference-only. Do not continue that conversation or send questions, status updates, or completion handoffs to the supervisor in normal assistant text.
|
|
18
|
+
|
|
19
|
+
Use contact_supervisor first. It resolves the supervisor session "{orchestratorTarget}" and run metadata automatically.
|
|
20
|
+
- If you cannot safely continue without one decision, approval, or product/API/scope clarification: contact_supervisor({ reason: "need_decision", message: "<question>" }). It steers the supervisor and keeps this ephemeral child alive for the reply.
|
|
21
|
+
- If you cannot safely continue until the supervisor provides multiple structured answers: contact_supervisor({ reason: "interview_request", message: "<context>", interview: { questions: [{ id: "<id>", type: "text", question: "<question>" }] } }). It also steers and keeps this child alive.
|
|
22
|
+
- After blocking contact_supervisor decisions or interviews, continue only after the reply arrives. Do not finish your final response with a choose-one question.
|
|
23
|
+
- Do not ask for clarification when the only conflict is review-only/no-edit versus progress-writing or artifact-writing instructions. Review-only/no-edit wins; leave files unchanged and mention the conflict in your final result only if it matters.
|
|
24
|
+
- A discovery or change the supervisor needs while working: contact_supervisor({ reason: "progress_update", message: "UPDATE: <finding and consequence>" }). This steers at the next tool boundary without waiting for a reply. Skip starts, redundant status, and routine completion; retain material findings in your final result.
|
|
25
|
+
- Generic intercom is lower-level fallback only; call load_intercom({}) first if intercom is inactive. Use blocking intercom({ action: "ask", to: "{orchestratorTarget}", delivery: "steer", message: "<question>" }) only when this child must remain alive for the answer.
|
|
26
|
+
- Treat a supervisor nudge as supplemental coordination within the active task: incorporate relevant context and continue. Replace the task only when the nudge explicitly says so. If it requests an answer, respond with intercom({ action: "send", to: "{orchestratorTarget}", delivery: "steer", message: "<answer>" }).
|
|
27
|
+
|
|
28
|
+
Messages explicitly marked “Direct user message to this agent” come from the human through the owning parent's Agents view. Treat them as user direction, not peer advice, and answer in your own conversation; the human sees it without a parent relay. Regular peer messages retain their existing authority boundaries.
|
|
29
|
+
|
|
30
|
+
Do not use contact_supervisor or intercom for routine completion handoffs. If no coordination is needed, return a focused task result.`;
|
|
31
|
+
|
|
32
|
+
export interface IntercomBridgeState {
|
|
33
|
+
orchestratorTarget: string;
|
|
34
|
+
instruction: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolveIntercomSessionTarget(sessionName: string | undefined, sessionId: string): string {
|
|
38
|
+
const trimmedName = sessionName?.trim();
|
|
39
|
+
if (trimmedName) return trimmedName;
|
|
40
|
+
const normalizedSessionId = sessionId.startsWith("session-") ? sessionId.slice("session-".length) : sessionId;
|
|
41
|
+
return `${DEFAULT_INTERCOM_TARGET_PREFIX}-${normalizedSessionId.slice(0, 8)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolveOrchestratorIntercomTarget(events: IntercomEventBus, fallback: string): string {
|
|
45
|
+
if (typeof events.on !== "function" || typeof events.emit !== "function") return fallback;
|
|
46
|
+
const requestId = randomUUID();
|
|
47
|
+
let exactTarget: string | undefined;
|
|
48
|
+
let unsubscribe: (() => void) | undefined;
|
|
49
|
+
let failed = false;
|
|
50
|
+
try {
|
|
51
|
+
unsubscribe = events.on(SUBAGENT_INTERCOM_IDENTITY_RESPONSE_EVENT, (payload) => {
|
|
52
|
+
if (!payload || typeof payload !== "object") return;
|
|
53
|
+
const response = payload as { requestId?: unknown; sessionId?: unknown };
|
|
54
|
+
if (response.requestId === requestId && typeof response.sessionId === "string" && response.sessionId.trim()) exactTarget = response.sessionId;
|
|
55
|
+
});
|
|
56
|
+
events.emit(SUBAGENT_INTERCOM_IDENTITY_REQUEST_EVENT, { requestId });
|
|
57
|
+
} catch {
|
|
58
|
+
failed = true;
|
|
59
|
+
} finally {
|
|
60
|
+
try { unsubscribe?.(); } catch { failed = true; }
|
|
61
|
+
}
|
|
62
|
+
return failed ? fallback : exactTarget ?? fallback;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function sanitizeIntercomTargetPart(value: string): string {
|
|
66
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function resolveSubagentIntercomTarget(runId: string, agent: string, index?: number): string {
|
|
70
|
+
const stepSuffix = index !== undefined ? `-${index + 1}` : "";
|
|
71
|
+
return `subagent-${sanitizeIntercomTargetPart(agent)}-${sanitizeIntercomTargetPart(runId)}${stepSuffix}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isIntercomExtensionEntry(entry: string): boolean {
|
|
75
|
+
return entry.trim().replaceAll("\\", "/").toLowerCase().split("/").includes("pi-intercom");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function extensionSandboxAllowsIntercom(extensions: string[] | undefined): boolean {
|
|
79
|
+
return extensions === undefined || extensions.some(isIntercomExtensionEntry);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveBundledIntercomExtensions(extensions: string[] | undefined): string[] | undefined {
|
|
83
|
+
if (extensions === undefined) return undefined;
|
|
84
|
+
const resolved = [...new Set(extensions.map((entry) => isIntercomExtensionEntry(entry) ? BUNDLED_INTERCOM_EXTENSION_PATH : entry))];
|
|
85
|
+
return resolved.length === extensions.length && resolved.every((entry, index) => entry === extensions[index]) ? extensions : resolved;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildIntercomBridgeInstruction(orchestratorTarget: string): string {
|
|
89
|
+
return `${INTERCOM_BRIDGE_MARKER}
|
|
90
|
+
${DEFAULT_INTERCOM_BRIDGE_TEMPLATE.replaceAll("{orchestratorTarget}", orchestratorTarget).trim()}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function resolveIntercomBridge(orchestratorTarget: string): IntercomBridgeState {
|
|
94
|
+
const target = orchestratorTarget.trim();
|
|
95
|
+
return {
|
|
96
|
+
orchestratorTarget: target,
|
|
97
|
+
instruction: buildIntercomBridgeInstruction(target),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function applyIntercomBridgeToAgent<T extends Pick<AgentConfig, "systemPrompt" | "tools" | "extensions">>(agent: T, bridge: IntercomBridgeState): T {
|
|
102
|
+
if (!extensionSandboxAllowsIntercom(agent.extensions)) return agent;
|
|
103
|
+
|
|
104
|
+
const bridgeTools = ["intercom", "contact_supervisor"];
|
|
105
|
+
const missingTools = agent.tools ? bridgeTools.filter((tool) => !agent.tools?.includes(tool)) : [];
|
|
106
|
+
const tools = agent.tools && missingTools.length ? [...agent.tools, ...missingTools] : agent.tools;
|
|
107
|
+
const extensions = resolveBundledIntercomExtensions(agent.extensions);
|
|
108
|
+
const instruction = bridge.instruction;
|
|
109
|
+
const trimmedPrompt = agent.systemPrompt?.trim() || "";
|
|
110
|
+
const systemPrompt = trimmedPrompt.includes(INTERCOM_BRIDGE_MARKER)
|
|
111
|
+
? trimmedPrompt
|
|
112
|
+
: trimmedPrompt
|
|
113
|
+
? `${trimmedPrompt}\n\n${instruction}`
|
|
114
|
+
: instruction;
|
|
115
|
+
|
|
116
|
+
if (tools === agent.tools && systemPrompt === agent.systemPrompt && extensions === agent.extensions) return agent;
|
|
117
|
+
return {
|
|
118
|
+
...agent,
|
|
119
|
+
tools,
|
|
120
|
+
systemPrompt,
|
|
121
|
+
extensions,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export interface LiveMessageReceipt {
|
|
4
|
+
delivered: boolean;
|
|
5
|
+
accepted?: boolean;
|
|
6
|
+
queued?: boolean;
|
|
7
|
+
messageId?: string;
|
|
8
|
+
reason?: string;
|
|
9
|
+
}
|
|
10
|
+
import {
|
|
11
|
+
SUBAGENT_INTERCOM_HEALTH_REQUEST_EVENT,
|
|
12
|
+
SUBAGENT_INTERCOM_HEALTH_RESPONSE_EVENT,
|
|
13
|
+
SUBAGENT_LIVE_INTERCOM_DELIVERY_EVENT,
|
|
14
|
+
SUBAGENT_LIVE_INTERCOM_EVENT,
|
|
15
|
+
type IntercomEventBus,
|
|
16
|
+
type SubagentLiveIntercomHealth,
|
|
17
|
+
type SubagentIntercomConnection,
|
|
18
|
+
} from "../shared/types.ts";
|
|
19
|
+
|
|
20
|
+
export async function sendLiveSubagentMessage(events: IntercomEventBus, input: {
|
|
21
|
+
to: string;
|
|
22
|
+
message: string;
|
|
23
|
+
delivery?: "steer" | "queue";
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
extra?: Record<string, unknown>;
|
|
27
|
+
}): Promise<LiveMessageReceipt> {
|
|
28
|
+
if (typeof events.on !== "function" || typeof events.emit !== "function") return { delivered: false, reason: "pi-intercom bridge unavailable" };
|
|
29
|
+
const requestId = randomUUID();
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
let settled = false;
|
|
32
|
+
let unsubscribe: (() => void) | undefined;
|
|
33
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
34
|
+
const finish = (result: LiveMessageReceipt) => {
|
|
35
|
+
if (settled) return;
|
|
36
|
+
settled = true;
|
|
37
|
+
if (timer) clearTimeout(timer);
|
|
38
|
+
input.signal?.removeEventListener("abort", abort);
|
|
39
|
+
unsubscribe?.();
|
|
40
|
+
resolve(result);
|
|
41
|
+
};
|
|
42
|
+
const abort = () => finish({ delivered: false, reason: "Message view/session closed; delivery not confirmed." });
|
|
43
|
+
unsubscribe = events.on(SUBAGENT_LIVE_INTERCOM_DELIVERY_EVENT, (data) => {
|
|
44
|
+
if (!data || typeof data !== "object") return;
|
|
45
|
+
const payload = data as LiveMessageReceipt & { requestId?: unknown };
|
|
46
|
+
if (payload.requestId !== requestId) return;
|
|
47
|
+
finish({ delivered: payload.delivered === true,
|
|
48
|
+
...(typeof payload.accepted === "boolean" ? { accepted: payload.accepted } : {}),
|
|
49
|
+
...(typeof payload.queued === "boolean" ? { queued: payload.queued } : {}),
|
|
50
|
+
...(typeof payload.messageId === "string" ? { messageId: payload.messageId } : {}),
|
|
51
|
+
...(typeof payload.reason === "string" ? { reason: payload.reason } : {}) });
|
|
52
|
+
});
|
|
53
|
+
timer = setTimeout(() => finish({ delivered: false, reason: "pi-intercom bridge unavailable" }), input.timeoutMs ?? 500);
|
|
54
|
+
if (input.signal?.aborted) { abort(); return; }
|
|
55
|
+
input.signal?.addEventListener("abort", abort, { once: true });
|
|
56
|
+
try {
|
|
57
|
+
events.emit(SUBAGENT_LIVE_INTERCOM_EVENT, {
|
|
58
|
+
...input.extra,
|
|
59
|
+
requestId,
|
|
60
|
+
to: input.to,
|
|
61
|
+
message: input.message,
|
|
62
|
+
delivery: input.delivery ?? "steer",
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
finish({ delivered: false, reason: error instanceof Error ? error.message : String(error) });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function queryLiveIntercomHealth(events: IntercomEventBus, targets: string[], timeoutMs = 300): Promise<Map<string, SubagentLiveIntercomHealth>> {
|
|
71
|
+
if (!targets.some((target) => target.trim())) return new Map();
|
|
72
|
+
return (await queryLiveIntercomStatus(events, targets, timeoutMs)).health;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function queryLiveIntercomStatus(events: IntercomEventBus, targets: string[] = [], timeoutMs = 300): Promise<{ health: Map<string, SubagentLiveIntercomHealth>; connection?: SubagentIntercomConnection }> {
|
|
76
|
+
const uniqueTargets = [...new Set(targets.map((target) => target.trim()).filter(Boolean))];
|
|
77
|
+
if (typeof events.on !== "function" || typeof events.emit !== "function") return { health: new Map() };
|
|
78
|
+
const requestId = randomUUID();
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
let settled = false;
|
|
81
|
+
let unsubscribe: (() => void) | undefined;
|
|
82
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
83
|
+
const finish = (items: SubagentLiveIntercomHealth[] = [], connection?: SubagentIntercomConnection) => {
|
|
84
|
+
if (settled) return;
|
|
85
|
+
settled = true;
|
|
86
|
+
if (timer) clearTimeout(timer);
|
|
87
|
+
unsubscribe?.();
|
|
88
|
+
resolve({ health: new Map(items.map((item) => [item.target, item])), connection });
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
unsubscribe = events.on(SUBAGENT_INTERCOM_HEALTH_RESPONSE_EVENT, (data) => {
|
|
92
|
+
if (!data || typeof data !== "object") return;
|
|
93
|
+
const payload = data as { requestId?: unknown; health?: unknown; connection?: SubagentIntercomConnection };
|
|
94
|
+
if (payload.requestId !== requestId || !Array.isArray(payload.health)) return;
|
|
95
|
+
const connection = payload.connection && ["connected", "disconnected", "connecting", "unknown"].includes(payload.connection.status) ? payload.connection : undefined;
|
|
96
|
+
finish(payload.health.filter((item): item is SubagentLiveIntercomHealth => Boolean(item && typeof item === "object" && typeof (item as { target?: unknown }).target === "string")), connection);
|
|
97
|
+
});
|
|
98
|
+
timer = setTimeout(() => finish(), timeoutMs);
|
|
99
|
+
events.emit(SUBAGENT_INTERCOM_HEALTH_REQUEST_EVENT, { requestId, targets: uniqueTargets });
|
|
100
|
+
} catch {
|
|
101
|
+
finish();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|