@fitchmultz/pi-subagents 0.44.4 → 0.44.6
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 +16 -0
- package/README.md +4 -0
- package/dist/extension/build-info.js +1 -1
- package/dist/extension/compact-tools.js +1 -1
- package/dist/extension/schemas.js +9 -9
- package/dist/history/index.js +14 -9
- package/dist/history/ingest.js +26 -5
- package/dist/history/queries.js +24 -4
- package/dist/history/worker.js +40 -14
- package/dist/pi-intercom/bridge.js +750 -0
- package/dist/runs/foreground/question-control.js +23 -12
- package/dist/runs/foreground/wait-run.js +5 -3
- package/dist/runs/shared/run-history.js +8 -1
- package/dist/runs/shared/supervisor-questions.js +32 -2
- package/dist/shared/status-format.js +1 -1
- package/docs/intercom-bridge.md +212 -0
- package/docs/intercom.md +3 -1
- package/docs/performance.md +3 -3
- package/package.json +2 -1
- package/scripts/intercom-bridge-credentials.mjs +143 -0
- package/scripts/intercom-remote.mjs +103 -0
- package/scripts/package-smoke.mjs +4 -0
- package/src/extension/compact-tools.ts +1 -1
- package/src/extension/schemas.ts +9 -9
- package/src/history/index.ts +13 -9
- package/src/history/ingest.ts +22 -5
- package/src/history/queries.ts +22 -4
- package/src/history/worker.ts +37 -13
- package/src/pi-intercom/bridge.ts +668 -0
- package/src/runs/foreground/question-control.ts +23 -11
- package/src/runs/foreground/wait-run.ts +5 -2
- package/src/runs/shared/run-history.ts +8 -1
- package/src/runs/shared/supervisor-questions.ts +45 -2
- package/src/shared/status-format.ts +2 -2
- package/src/shared/types.ts +2 -1
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { IntercomEventBus, SubagentExecutionResult } from "../../shared/types.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { formatRunAction } from "../../shared/status-format.ts";
|
|
5
|
+
import { cancelSupervisorQuestion, claimQuestionRevival, formatSupervisorQuestions, listSupervisorQuestions, questionProcessAlive, questionRecoveryHint, readQuestionState, recordQuestionDelivery, releaseQuestionRevival, saveQuestionAnswer, supervisorQuestionPage, type SupervisorQuestionView } from "../shared/supervisor-questions.ts";
|
|
5
6
|
import { liveLaunchOverrideNotice, nestedResolutionScopeForExecutor, reviveSavedSubagent } from "./foreground-control.ts";
|
|
6
7
|
import type { ExecutorDeps, SubagentParamsLike } from "./subagent-params.ts";
|
|
7
8
|
|
|
8
|
-
function questionResult(questions: SupervisorQuestionView[], text
|
|
9
|
-
return { content: [{ type: "text", text }], details: { mode: "management", results: [], questions } };
|
|
9
|
+
function questionResult(questions: SupervisorQuestionView[], text: string): SubagentExecutionResult {
|
|
10
|
+
return { content: [{ type: "text", text }], details: { mode: "management", results: [], ...supervisorQuestionPage(questions) } };
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export function projectSupervisorQuestions(result: SubagentExecutionResult, params: SubagentParamsLike, ownerSessionId: string, childSafe = false): SubagentExecutionResult {
|
|
13
14
|
if (result.isError && !result.content.some((item) => item.type === "text" && /Async run not found|Status file not found/.test(item.text))) return result;
|
|
14
|
-
const
|
|
15
|
+
const id = params.id ?? params.runId ?? (params.dir ? path.basename(params.dir) : undefined);
|
|
16
|
+
const questions = listSupervisorQuestions(ownerSessionId, id)
|
|
15
17
|
.filter((question) => question.state === "awaiting_input" || question.state === "answer_pending");
|
|
16
18
|
if (!questions.length) return result;
|
|
19
|
+
const page = supervisorQuestionPage(questions);
|
|
17
20
|
const questionOnly = result.isError && result.content.some((item) => item.type === "text" && /Async run not found|Status file not found/.test(item.text));
|
|
18
21
|
return {
|
|
19
22
|
...result,
|
|
20
23
|
...(questionOnly ? { isError: false } : {}),
|
|
21
|
-
content: [{ type: "text", text: [`Supervisor input (not execution completion):\n${formatSupervisorQuestions(questions, childSafe)}`,
|
|
22
|
-
|
|
24
|
+
content: [{ type: "text", text: [`Supervisor input (not execution completion):\n${formatSupervisorQuestions(page.questions, childSafe)}`,
|
|
25
|
+
...(page.questionList.nextOffset !== undefined ? [`More questions: ${formatRunAction("questions", id, { offset: page.questionList.nextOffset, limit: page.questionList.limit }, childSafe)}`] : []),
|
|
26
|
+
...(questionOnly ? [] : result.content.map((item) => item.type === "text" ? item.text : ""))].join("\n\n") }],
|
|
27
|
+
details: { ...result.details, ...page },
|
|
23
28
|
};
|
|
24
29
|
}
|
|
25
30
|
|
|
@@ -35,7 +40,7 @@ export function cancelSupervisorInput(result: SubagentExecutionResult, params: S
|
|
|
35
40
|
}
|
|
36
41
|
const cancelled = questions.map((question) => readQuestionState(question));
|
|
37
42
|
return result.isError ? questionResult(cancelled, `Cancelled ${questions.length} pending supervisor question(s). Any live waiter will abort its agent; cancellation is requested, not proof of process exit.`)
|
|
38
|
-
: { ...result, details: { ...result.details,
|
|
43
|
+
: { ...result, details: { ...result.details, ...supervisorQuestionPage(cancelled) } };
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
export function controlSupervisorQuestion(input: { params: SubagentParamsLike; requestCwd: string; ctx: ExtensionContext; deps: ExecutorDeps }): SubagentExecutionResult {
|
|
@@ -45,8 +50,15 @@ export function controlSupervisorQuestion(input: { params: SubagentParamsLike; r
|
|
|
45
50
|
if (params.dir !== undefined) throw new Error("questions/answer use a run id, not dir.");
|
|
46
51
|
if (params.index !== undefined && (!Number.isSafeInteger(params.index) || params.index < 0)) throw new Error("index must be a non-negative integer.");
|
|
47
52
|
const questions = listSupervisorQuestions(input.ctx.sessionManager.getSessionId(), id)
|
|
48
|
-
.filter((question) => params.index === undefined || question.index === params.index);
|
|
49
|
-
if (params.action === "questions")
|
|
53
|
+
.filter((question) => (params.index === undefined || question.index === params.index) && (params.action !== "questions" || params.questionId === undefined || question.questionId === params.questionId));
|
|
54
|
+
if (params.action === "questions") {
|
|
55
|
+
const page = supervisorQuestionPage(questions, params), childSafe = Boolean(nestedResolutionScopeForExecutor(input.deps));
|
|
56
|
+
const text = [formatSupervisorQuestions(page.questions, childSafe),
|
|
57
|
+
`Questions: ${page.questions.length} shown, ${page.questionList.total} total (offset ${page.questionList.offset}).`,
|
|
58
|
+
...(page.questionList.nextOffset !== undefined ? [`More questions: ${formatRunAction("questions", id, { offset: page.questionList.nextOffset, limit: page.questionList.limit,
|
|
59
|
+
...(params.index !== undefined ? { index: params.index } : {}), ...(params.questionId ? { questionId: params.questionId } : {}) }, childSafe)}`] : [])].join("\n\n");
|
|
60
|
+
return { content: [{ type: "text", text }], details: { mode: "management", results: [], ...page } };
|
|
61
|
+
}
|
|
50
62
|
if (!id || !params.questionId) throw new Error("action='answer' requires id and questionId.");
|
|
51
63
|
const question = questions.find((entry) => entry.questionId === params.questionId);
|
|
52
64
|
if (!question) throw new Error("Question not found in this session's runs. Resume the owning supervisor session to answer it.");
|
|
@@ -60,10 +72,10 @@ export function controlSupervisorQuestion(input: { params: SubagentParamsLike; r
|
|
|
60
72
|
const result = reviveSavedSubagent({ ...input, params: { ...params, message: `${answer.origin === "human" ? "Direct user answer (human origin)" : "Supervisor answer"} to question ${question.questionId}:\n\n${answer.message}\n\nOriginal question:\n${question.message}` } }, { ...question, source: "question" }, claim.runId);
|
|
61
73
|
if (result.isError) {
|
|
62
74
|
releaseQuestionRevival(question);
|
|
63
|
-
return { ...result, details: { ...result.details,
|
|
75
|
+
return { ...result, details: { ...result.details, ...supervisorQuestionPage([readQuestionState(question)]) } };
|
|
64
76
|
}
|
|
65
77
|
recordQuestionDelivery(question, { kind: "revive", runId: claim.runId, deliveredAt: Date.now() });
|
|
66
|
-
return { ...result, details: { ...result.details,
|
|
78
|
+
return { ...result, details: { ...result.details, ...supervisorQuestionPage([readQuestionState(question)]) } };
|
|
67
79
|
} catch (error) {
|
|
68
80
|
return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "management", results: [] } };
|
|
69
81
|
}
|
|
@@ -2,7 +2,8 @@ import * as path from "node:path";
|
|
|
2
2
|
import { writeAsyncControlRequest } from "../background/async-control.ts";
|
|
3
3
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { ownedRunExecutionResult, ownedRunProgressResult, ownedRunStatusResult, ownedRunView, resolveOwnedRun } from "../shared/run-records.ts";
|
|
5
|
-
import { getRunMetadataDir, listOwnedRunQuestions, questionProcessAlive, readRunJson } from "../shared/supervisor-questions.ts";
|
|
5
|
+
import { formatSupervisorQuestions, getRunMetadataDir, listOwnedRunQuestions, questionProcessAlive, readRunJson, supervisorQuestionPage } from "../shared/supervisor-questions.ts";
|
|
6
|
+
import { formatRunAction } from "../../shared/status-format.ts";
|
|
6
7
|
import { getSingleResultOutput, readStatus } from "../../shared/utils.ts";
|
|
7
8
|
import { INTERCOM_DETACH_REQUEST_EVENT, INTERCOM_DETACH_RESPONSE_EVENT, POLL_INTERVAL_MS, type OwnedRun, type SubagentExecutionResult } from "../../shared/types.ts";
|
|
8
9
|
import { resolveSubagentRunId } from "../background/run-id-resolver.ts";
|
|
@@ -86,7 +87,9 @@ export async function waitForOwnedRun(input: {
|
|
|
86
87
|
const questions = runQuestions.filter((question) => question.ownerSessionId === owner && (index === undefined || question.index === index) && question.state === "awaiting_input");
|
|
87
88
|
if (questions.length) {
|
|
88
89
|
const result = ownedRunStatusResult(target, deps.state);
|
|
89
|
-
|
|
90
|
+
const page = supervisorQuestionPage(questions), childSafe = Boolean(nestedResolutionScopeForExecutor(deps));
|
|
91
|
+
Object.assign(result.details, page);
|
|
92
|
+
finish("awaiting_input", `Run ${target.runId} needs input; waiting ended without stopping it.\n\n${formatSupervisorQuestions(page.questions, childSafe)}\n\nAll questions: ${formatRunAction("questions", target.runId, index === undefined ? {} : { index }, childSafe)}`, result); return;
|
|
90
93
|
}
|
|
91
94
|
if ((index !== undefined || view.resultPath) && children.every((child) => child.result && child.state !== "live" && child.state !== "unknown") && (index !== undefined || view.state !== "live")) {
|
|
92
95
|
const result = ownedRunStatusResult(target, deps.state);
|
|
@@ -63,6 +63,12 @@ export function recordRun(agent: string, task: string, exitCode: number, duratio
|
|
|
63
63
|
const agentDir = getAgentDir(); fs.mkdirSync(agentDir, { recursive: true });
|
|
64
64
|
db = openHistoryDatabase(path.join(historyDirectory(agentDir), TIMING_DATABASE));
|
|
65
65
|
const connection = db;
|
|
66
|
+
const imported = connection.prepare("SELECT 1 FROM sqlite_schema WHERE type='table' AND name='timing_meta'").get()
|
|
67
|
+
&& connection.prepare("SELECT key FROM timing_meta WHERE key='legacy-imported'").get();
|
|
68
|
+
// Filesystem reads and JSON parsing must not consume another writer's admission budget.
|
|
69
|
+
const legacy = imported ? [] : legacySamples(agentDir).reverse();
|
|
70
|
+
// Allow brief native writer overlap; three attempts still bound best-effort admission.
|
|
71
|
+
connection.exec("PRAGMA busy_timeout=1000");
|
|
66
72
|
historyTransaction(connection, () => {
|
|
67
73
|
connection.exec(`CREATE TABLE IF NOT EXISTS timing_meta(key TEXT PRIMARY KEY);
|
|
68
74
|
CREATE TABLE IF NOT EXISTS samples(sequence INTEGER PRIMARY KEY,agent TEXT NOT NULL,task TEXT NOT NULL,ts INTEGER NOT NULL,status TEXT NOT NULL,duration REAL NOT NULL,exit INTEGER);
|
|
@@ -70,7 +76,8 @@ export function recordRun(agent: string, task: string, exitCode: number, duratio
|
|
|
70
76
|
const insert = connection.prepare("INSERT INTO samples(agent,task,ts,status,duration,exit) VALUES (?,?,?,?,?,?)");
|
|
71
77
|
const save = (sample: RunEntry) => insert.run(sample.agent, sample.task, sample.ts, sample.status, sample.duration, sample.exit ?? null);
|
|
72
78
|
if (!connection.prepare("SELECT key FROM timing_meta WHERE key='legacy-imported'").get()) {
|
|
73
|
-
|
|
79
|
+
// Another writer may have imported while this snapshot was being read.
|
|
80
|
+
for (const sample of legacy) save(sample);
|
|
74
81
|
connection.exec("INSERT INTO timing_meta VALUES ('legacy-imported')");
|
|
75
82
|
}
|
|
76
83
|
save(entry);
|
|
@@ -75,6 +75,46 @@ export interface SupervisorQuestionView extends SupervisorQuestion {
|
|
|
75
75
|
revival?: { runId: string; pid: number; startedAt: number };
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
export interface SupervisorQuestionSummary extends Pick<SupervisorQuestionView,
|
|
79
|
+
"questionId" | "runId" | "ownerSessionId" | "ownerTarget" | "agent" | "index" | "childSessionId" | "childTarget" |
|
|
80
|
+
"sessionFile" | "cwd" | "pid" | "processIdentity" | "createdAt" | "reason" | "message" | "state" | "answer" | "delivery" | "revival"> {
|
|
81
|
+
questionPath: string;
|
|
82
|
+
answerPath?: string;
|
|
83
|
+
messageTruncated?: boolean;
|
|
84
|
+
answerTruncated?: boolean;
|
|
85
|
+
hasInterview?: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function summarizeSupervisorQuestion(question: SupervisorQuestionView): SupervisorQuestionSummary {
|
|
89
|
+
const directory = questionDir(question);
|
|
90
|
+
return {
|
|
91
|
+
questionId: question.questionId, runId: question.runId, ownerSessionId: question.ownerSessionId, ownerTarget: question.ownerTarget,
|
|
92
|
+
agent: question.agent, index: question.index, childSessionId: question.childSessionId, childTarget: question.childTarget,
|
|
93
|
+
sessionFile: question.sessionFile, cwd: question.cwd, pid: question.pid, processIdentity: question.processIdentity,
|
|
94
|
+
createdAt: question.createdAt, reason: question.reason, state: question.state, message: question.message.slice(0, 2048),
|
|
95
|
+
questionPath: path.join(directory, "question.json"),
|
|
96
|
+
...(question.message.length > 2048 ? { messageTruncated: true } : {}),
|
|
97
|
+
...(question.interview ? { hasInterview: true } : {}),
|
|
98
|
+
...(question.answer ? { answer: { message: question.answer.message.slice(0, 2048), answeredAt: question.answer.answeredAt, origin: question.answer.origin },
|
|
99
|
+
answerPath: path.join(directory, "answer.json"), ...(question.answer.message.length > 2048 ? { answerTruncated: true } : {}) } : {}),
|
|
100
|
+
...(question.delivery ? { delivery: { kind: question.delivery.kind, runId: question.delivery.runId, deliveredAt: question.delivery.deliveredAt } } : {}),
|
|
101
|
+
...(question.revival ? { revival: { runId: question.revival.runId, pid: question.revival.pid, startedAt: question.revival.startedAt } } : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function supervisorQuestionPage(questions: SupervisorQuestionView[], options: { offset?: number; limit?: number } = {}): {
|
|
106
|
+
questions: SupervisorQuestionSummary[];
|
|
107
|
+
questionList: { total: number; offset: number; limit: number; nextOffset?: number };
|
|
108
|
+
} {
|
|
109
|
+
const offset = options.offset ?? 0, limit = options.limit ?? 20;
|
|
110
|
+
if (!Number.isSafeInteger(offset) || offset < 0) throw new Error("Question offset must be a non-negative integer.");
|
|
111
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) throw new Error("Question limit must be from 1 to 100.");
|
|
112
|
+
const pending = (question: SupervisorQuestionView) => question.state === "awaiting_input" || question.state === "answer_pending" ? 0 : 1;
|
|
113
|
+
const sorted = questions.toSorted((a, b) => pending(a) - pending(b) || a.createdAt - b.createdAt || a.questionId.localeCompare(b.questionId));
|
|
114
|
+
return { questions: sorted.slice(offset, offset + limit).map((question) => summarizeSupervisorQuestion(question)),
|
|
115
|
+
questionList: { total: questions.length, offset, limit, ...(offset + limit < questions.length ? { nextOffset: offset + limit } : {}) } };
|
|
116
|
+
}
|
|
117
|
+
|
|
78
118
|
function safeId(value: string): string {
|
|
79
119
|
if (typeof value !== "string" || !/^[a-zA-Z0-9_-]+$/.test(value)) throw new Error("Invalid run or question ID.");
|
|
80
120
|
return value;
|
|
@@ -356,17 +396,20 @@ export function questionRecoveryHint(question: SupervisorQuestion, childSafe = f
|
|
|
356
396
|
return `Recover an unlaunched continuation: ${formatRunAction("resume", question.runId, { index: question.index, message: "Continue with the saved supervisor answer." }, childSafe)}`;
|
|
357
397
|
}
|
|
358
398
|
|
|
359
|
-
export function formatSupervisorQuestions(questions:
|
|
399
|
+
export function formatSupervisorQuestions(questions: SupervisorQuestionSummary[], childSafe = false): string {
|
|
360
400
|
if (!questions.length) return "No supervisor questions owned by this session.";
|
|
361
401
|
return questions.map((question) => [
|
|
362
402
|
`Question: ${question.questionId} | ${question.state}`,
|
|
363
403
|
`Run: ${question.runId} | Child: ${question.index} (${question.agent}) | Owner: ${question.ownerSessionId}`,
|
|
364
404
|
`Session: ${question.sessionFile} | Child target: ${question.childTarget}`,
|
|
365
405
|
question.message,
|
|
366
|
-
...(question.
|
|
406
|
+
...(question.messageTruncated ? [`[Question preview truncated. Full question: ${question.questionPath}]`] : []),
|
|
407
|
+
...(question.hasInterview ? [`Structured interview: ${question.questionPath}`] : []),
|
|
408
|
+
...(question.answer ? [`Saved answer: ${question.answer.message}`, ...(question.answerTruncated ? [`[Answer preview truncated. Exact saved answer: ${question.answerPath}]`] : [])] : []),
|
|
367
409
|
...(question.revival && !question.delivery ? [`Continuation requested: ${question.revival.runId}; delivery unconfirmed. Answer retained.`, questionRecoveryHint(question, childSafe)] : []),
|
|
368
410
|
question.delivery ? `Answer delivered via ${question.delivery.kind}; run: ${question.delivery.runId}`
|
|
369
411
|
: question.state === "cancelled" ? "Question cancelled; use continue for a new follow-up."
|
|
412
|
+
: question.answerTruncated ? `Retry answer with the exact saved message from ${question.answerPath}; a preview is not a valid replacement.`
|
|
370
413
|
: `Answer: ${formatRunAction("answer", question.runId, { questionId: question.questionId, message: question.answer?.message ?? "..." }, childSafe)}`,
|
|
371
414
|
].join("\n")).join("\n\n");
|
|
372
415
|
}
|
|
@@ -6,11 +6,11 @@ export function formatAgentProcessExit(exit: AgentProcessExit | undefined): stri
|
|
|
6
6
|
: "Agent process exit not recorded. Command exit is unconfirmed.";
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export function formatRunAction(action: ManagementAction | "questions" | "answer", id: string, fields: Record<string, string | number | boolean> = {}, childSafe = false): string {
|
|
9
|
+
export function formatRunAction(action: ManagementAction | "questions" | "answer", id: string | undefined, fields: Record<string, string | number | boolean> = {}, childSafe = false): string {
|
|
10
10
|
const legacyChild = childSafe && loadConfig().compactChildTools === false;
|
|
11
11
|
const parentAction = { status: "inspect", resume: "continue", interrupt: "stop" };
|
|
12
12
|
const name = legacyChild ? action : parentAction[action as keyof typeof parentAction] ?? action;
|
|
13
|
-
const args = Object.entries({ action: name, id, ...fields }).map(([key, value]) => `${key}: ${JSON.stringify(value)}`).join(", ");
|
|
13
|
+
const args = Object.entries({ action: name, ...(id !== undefined ? { id } : {}), ...fields }).map(([key, value]) => `${key}: ${JSON.stringify(value)}`).join(", ");
|
|
14
14
|
const call = `${legacyChild || action === "extend" ? "subagent" : "agent_runs"}({ ${args} })`;
|
|
15
15
|
return action === "extend" && !legacyChild ? `load_subagent({}), then ${call}` : call;
|
|
16
16
|
}
|
package/src/shared/types.ts
CHANGED
|
@@ -580,7 +580,8 @@ export interface Details {
|
|
|
580
580
|
intercomTargets?: string[];
|
|
581
581
|
managementControl?: ManagementControl;
|
|
582
582
|
managementControls?: ManagementControl[];
|
|
583
|
-
questions?: import("../runs/shared/supervisor-questions.ts").
|
|
583
|
+
questions?: import("../runs/shared/supervisor-questions.ts").SupervisorQuestionSummary[];
|
|
584
|
+
questionList?: { total: number; offset: number; limit: number; nextOffset?: number };
|
|
584
585
|
wait?: { runId: string; completionId?: string; index?: number; status: "completed" | "cancelled" | "yielded" | "awaiting_input" | "unavailable" };
|
|
585
586
|
run?: OwnedRunView;
|
|
586
587
|
runs?: Array<Pick<OwnedRunView, "runId" | "source" | "mode" | "cwd" | "task" | "state" | "updatedAt" | "attention" | "review" | "rootRunId" | "predecessorRunId" | "predecessorIndex"> & { summary?: string; continuations?: string[] }>;
|