@xingular/harness 0.1.7 → 2.1.0
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 +20 -0
- package/README.md +36 -13
- package/dist/cli.js +10728 -8033
- package/dist/terminal-pi.js +365 -24
- package/docs/README.md +3 -0
- package/docs/decisions/ADR-0002-native-subagent-lifecycle.md +49 -0
- package/docs/decisions/ADR-0003-shared-asynchronous-subagents.md +70 -0
- package/docs/decisions/ADR-0004-fullscreen-terminal-layout.md +49 -0
- package/docs/decisions/ADR-0005-app-provided-oauth-clients.md +54 -0
- package/docs/terminal-tui.md +109 -13
- package/package.json +5 -4
- package/src/accounts/accounts.test.ts +87 -1
- package/src/accounts/accounts.ts +120 -18
- package/src/core/bus.test.ts +22 -0
- package/src/core/bus.ts +17 -2
- package/src/harness.ts +12 -2
- package/src/hub/hub.test.ts +49 -1
- package/src/hub/hub.ts +122 -6
- package/src/hub/schema.ts +13 -1
- package/src/index.ts +7 -2
- package/src/mcp/connections.ts +39 -12
- package/src/mcp/service.test.ts +49 -3
- package/src/mcp/service.ts +14 -3
- package/src/mcp/types.ts +10 -0
- package/src/memory/curation.test.ts +44 -1
- package/src/memory/dispatch.ts +5 -0
- package/src/memory/maintenance.test.ts +17 -1
- package/src/memory/maintenance.ts +169 -8
- package/src/memory/recall.ts +2 -1
- package/src/memory/trajectory.test.ts +178 -0
- package/src/memory/trajectory.ts +220 -0
- package/src/memory/wiki.ts +2 -1
- package/src/memory/wikiIndex.ts +3 -2
- package/src/plugins/builtin.ts +4 -2
- package/src/plugins/catalogue.test.ts +1 -1
- package/src/server/methods.ts +8 -0
- package/src/server/server.test.ts +20 -1
- package/src/sessions/approvals.test.ts +49 -0
- package/src/sessions/approvals.ts +26 -4
- package/src/sessions/conversation.test.ts +56 -0
- package/src/sessions/lineageJoin.ts +1 -1
- package/src/sessions/planMode.test.ts +40 -0
- package/src/sessions/planMode.ts +57 -0
- package/src/sessions/registry.ts +38 -7
- package/src/sessions/runStore.ts +14 -0
- package/src/sessions/runner.ts +9 -3
- package/src/sessions/sessions.test.ts +69 -7
- package/src/sessions/sessions.ts +47 -39
- package/src/sessions/subagentService.test.ts +159 -0
- package/src/sessions/subagentService.ts +236 -0
- package/src/sessions/subagents.ts +97 -0
- package/src/terminal/controller.ts +73 -10
- package/src/terminal/piExtension.ts +42 -6
- package/src/terminal/socket.test.ts +23 -0
- package/src/terminal/socket.ts +42 -18
- package/src/terminal/standalone.ts +12 -0
- package/src/terminal/terminal.test.ts +150 -15
- package/src/terminal/terminals.ts +113 -5
- package/src/terminal/trajectory.test.ts +66 -0
- package/src/terminal/trajectory.ts +44 -0
- package/src/terminal/tui/application.test.ts +206 -9
- package/src/terminal/tui/application.ts +251 -81
- package/src/terminal/tui/commands.test.ts +70 -9
- package/src/terminal/tui/commands.ts +99 -7
- package/src/terminal/tui/editor.test.ts +56 -27
- package/src/terminal/tui/editor.ts +64 -19
- package/src/terminal/tui/keybindings.test.ts +7 -0
- package/src/terminal/tui/keybindings.ts +9 -1
- package/src/terminal/tui/planMode.test.ts +10 -0
- package/src/terminal/tui/planMode.ts +2 -0
- package/src/terminal/tui/runtime.socket.test.ts +77 -3
- package/src/terminal/tui/runtime.test.ts +10 -0
- package/src/terminal/tui/runtime.ts +24 -9
- package/src/terminal/tui/screen.test.ts +143 -0
- package/src/terminal/tui/screen.ts +121 -0
- package/src/terminal/tui/skillInvocation.test.ts +71 -0
- package/src/terminal/tui/skillInvocation.ts +51 -0
- package/src/terminal/tui/status.test.ts +24 -46
- package/src/terminal/tui/status.ts +42 -69
- package/src/terminal/tui/theme.ts +18 -7
- package/src/terminal/tui/trajectoryPanel.test.ts +99 -0
- package/src/terminal/tui/trajectoryPanel.ts +226 -0
- package/src/terminal/tui/transcript.test.ts +14 -0
- package/src/terminal/tui/transcript.ts +11 -0
- package/src/terminal/tui/ui.ts +2 -2
- package/src/terminal/tui/welcome.ts +4 -4
- package/src/terminal/tui/workingStatus.test.ts +44 -0
- package/src/terminal/tui/workingStatus.ts +238 -0
- package/src/tools/definitions.ts +42 -1
- package/src/tools/maintenance.ts +6 -1
- package/src/tools/spawnSubagent.ts +40 -85
- package/src/tools/types.ts +4 -0
- package/docs/evidence/terminal-2026-10-17/120x40-completed-transcript.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17/40x20-settings-interaction.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17/40x20-settings-keyboard.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17/40x20-welcome.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17/80x24-approval-0.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17/80x24-question.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17/verification.json +0 -130
- package/docs/evidence/terminal-2026-10-17-followup/120x40-completed-transcript.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/40x20-settings-interaction.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/40x20-settings-keyboard.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/40x20-welcome.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/80x24-approval-0.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/80x24-plugin-light.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/80x24-question-batch.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/80x24-question.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/app-bundle/80x24-approval-0.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/app-bundle/80x24-plugin-light.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/app-bundle/80x24-question-batch.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/app-bundle/80x24-question.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/app-bundle/verification.json +0 -95
- package/docs/evidence/terminal-2026-10-17-followup/global-install/80x24-approval-0.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/global-install/80x24-plugin-light.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/global-install/80x24-question-batch.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/global-install/80x24-question.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/global-install/README.md +0 -27
- package/docs/evidence/terminal-2026-10-17-followup/global-install/shell-command-80x24.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/global-install/shell-command.json +0 -18
- package/docs/evidence/terminal-2026-10-17-followup/global-install/verification.json +0 -95
- package/docs/evidence/terminal-2026-10-17-followup/harness-reference-80x24.json.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/harness-reference-final-80x24.json.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/harness-reference-scenario.json +0 -60
- package/docs/evidence/terminal-2026-10-17-followup/live-codex/README.md +0 -24
- package/docs/evidence/terminal-2026-10-17-followup/live-codex/complete-80x24.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/live-codex/verification.json +0 -57
- package/docs/evidence/terminal-2026-10-17-followup/omp-reference-80x24-early.json.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/omp-reference-80x24.json.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/omp-reference-scenario.json +0 -58
- package/docs/evidence/terminal-2026-10-17-followup/omp-supplemental-static.json.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/rea-artifacts.json +0 -320
- package/docs/evidence/terminal-2026-10-17-followup/reference-comparison.json +0 -30
- package/docs/evidence/terminal-2026-10-17-followup/reference-comparison.md +0 -133
- package/docs/evidence/terminal-2026-10-17-followup/reference-source-observations.json +0 -20
- package/docs/evidence/terminal-2026-10-17-followup/verification.json +0 -207
package/dist/terminal-pi.js
CHANGED
|
@@ -10,6 +10,7 @@ var SocketError = class extends Error {
|
|
|
10
10
|
super(message2);
|
|
11
11
|
this.code = code;
|
|
12
12
|
}
|
|
13
|
+
code;
|
|
13
14
|
};
|
|
14
15
|
function connectionFromEnv(env = process.env) {
|
|
15
16
|
const socket = env[SOCKET_ENV];
|
|
@@ -105,8 +106,8 @@ async function attempt(ui, action) {
|
|
|
105
106
|
return false;
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
|
-
function resetIn(resetsAt,
|
|
109
|
-
const minutes = Math.round((Date.parse(resetsAt) -
|
|
109
|
+
function resetIn(resetsAt, now2 = Date.now()) {
|
|
110
|
+
const minutes = Math.round((Date.parse(resetsAt) - now2) / 6e4);
|
|
110
111
|
if (Number.isNaN(minutes)) return resetsAt;
|
|
111
112
|
if (minutes <= 0) return "now";
|
|
112
113
|
if (minutes < 60) return `in ${minutes}m`;
|
|
@@ -123,7 +124,7 @@ function accountRows(overview, session) {
|
|
|
123
124
|
var limitLine = (limit) => `${limit.label}: ${limit.usedPercent ?? "?"}% used${limit.resetsAt ? `, resets ${resetIn(limit.resetsAt)}` : ""}`;
|
|
124
125
|
function accountDetails(account) {
|
|
125
126
|
const state = account.state;
|
|
126
|
-
const
|
|
127
|
+
const lines2 = [
|
|
127
128
|
account.label,
|
|
128
129
|
`Health: ${HEALTH[state.health] ?? HEALTH.unknown}`,
|
|
129
130
|
`Default for new sessions: ${account.isDefault ? "yes" : "no"}`,
|
|
@@ -137,7 +138,7 @@ function accountDetails(account) {
|
|
|
137
138
|
state.limits.length ? null : "Usage limits: Pi providers do not report them.",
|
|
138
139
|
state.capabilities.length ? `Detected: ${state.capabilities.join(", ")}` : null
|
|
139
140
|
];
|
|
140
|
-
return
|
|
141
|
+
return lines2.filter((line) => line !== null).join("\n");
|
|
141
142
|
}
|
|
142
143
|
function loginStep(step) {
|
|
143
144
|
if (step.type === "auth_url" && typeof step.url === "string") return `Open this link to sign in:
|
|
@@ -233,9 +234,9 @@ async function accountMenu(ui, call, session, accountId) {
|
|
|
233
234
|
}
|
|
234
235
|
async function showActivity(ui, call, accountId, label) {
|
|
235
236
|
const entries = await call("accounts.audit", { accountId, limit: 30 });
|
|
236
|
-
const
|
|
237
|
+
const lines2 = entries.map((entry) => `${new Date(entry.at).toLocaleString()} \xB7 ${entry.action} \xB7 ${entry.detail}`);
|
|
237
238
|
await ui.select(`${label}: activity
|
|
238
|
-
${
|
|
239
|
+
${lines2.join("\n") || "Nothing has happened here yet."}`, [BACK]);
|
|
239
240
|
}
|
|
240
241
|
async function signIn(ui, call, account) {
|
|
241
242
|
const providers = await call("accounts.providers");
|
|
@@ -284,7 +285,7 @@ async function followLogin(ui, call, accountId, loginId) {
|
|
|
284
285
|
await answerPrompt(ui, call, loginId, snapshot.prompt);
|
|
285
286
|
continue;
|
|
286
287
|
}
|
|
287
|
-
const step = [...snapshot.events].reverse().map(loginStep).find((
|
|
288
|
+
const step = [...snapshot.events].reverse().map(loginStep).find((text3) => text3 !== null);
|
|
288
289
|
if (step && snapshot.revision !== shownRevision) {
|
|
289
290
|
shownRevision = snapshot.revision;
|
|
290
291
|
const choice = await ui.select(`${step}
|
|
@@ -313,18 +314,18 @@ function reportLogin(ui, snapshot) {
|
|
|
313
314
|
async function answerPrompt(ui, call, loginId, prompt) {
|
|
314
315
|
const promptId = String(prompt.promptId ?? "");
|
|
315
316
|
const detail = prompt.prompt ?? {};
|
|
316
|
-
const
|
|
317
|
+
const text3 = detail.message ?? "Sign-in needs an answer.";
|
|
317
318
|
let value;
|
|
318
319
|
if (detail.type === "select" && Array.isArray(detail.options)) {
|
|
319
320
|
if (detail.options.every((option) => typeof option === "string")) {
|
|
320
|
-
value = await ui.select(
|
|
321
|
+
value = await ui.select(text3, detail.options);
|
|
321
322
|
} else if (detail.options.every((option) => typeof option === "object" && option !== null && "id" in option && typeof option.id === "string" && "label" in option && typeof option.label === "string")) {
|
|
322
323
|
const options = detail.options;
|
|
323
|
-
const selected = await ui.select(
|
|
324
|
+
const selected = await ui.select(text3, options.map((option) => option.label));
|
|
324
325
|
value = options.find((option) => option.label === selected)?.id;
|
|
325
326
|
}
|
|
326
327
|
} else {
|
|
327
|
-
value = await ui.input(
|
|
328
|
+
value = await ui.input(text3, detail.placeholder ?? void 0);
|
|
328
329
|
}
|
|
329
330
|
await call("accounts.loginRespond", { loginId, promptId, value: value ?? null });
|
|
330
331
|
}
|
|
@@ -497,11 +498,303 @@ function claimTerminalControl(connection = connectionFromEnv()) {
|
|
|
497
498
|
}
|
|
498
499
|
return claim;
|
|
499
500
|
}
|
|
501
|
+
async function controlCall(call, params = {}, timeoutMs = 12e4) {
|
|
502
|
+
const connection = connectionFromEnv();
|
|
503
|
+
const control = await claimTerminalControl(connection);
|
|
504
|
+
return socketCall(connection, call, params, timeoutMs, control);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/sessions/contracts.ts
|
|
508
|
+
import { z } from "zod";
|
|
509
|
+
var runStatusSchema = z.enum(["queued", "running", "waiting_for_approval", "waiting_for_input", "interrupted", "completed", "failed"]);
|
|
510
|
+
var runEventTypeSchema = z.enum([
|
|
511
|
+
"started",
|
|
512
|
+
"context.snapshot",
|
|
513
|
+
"message.delta",
|
|
514
|
+
"message.completed",
|
|
515
|
+
"message.steered",
|
|
516
|
+
"tool.started",
|
|
517
|
+
"tool.completed",
|
|
518
|
+
"retry.started",
|
|
519
|
+
"retry.ended",
|
|
520
|
+
"compaction.started",
|
|
521
|
+
"compaction.ended",
|
|
522
|
+
"approval.requested",
|
|
523
|
+
"approval.resolved",
|
|
524
|
+
"question.requested",
|
|
525
|
+
"question.answered",
|
|
526
|
+
"extension.notice",
|
|
527
|
+
"reaction.added",
|
|
528
|
+
"agent.message",
|
|
529
|
+
"artifact.added",
|
|
530
|
+
"file.changed",
|
|
531
|
+
"usage.updated",
|
|
532
|
+
"account.status",
|
|
533
|
+
"status.changed",
|
|
534
|
+
"failed",
|
|
535
|
+
"completed"
|
|
536
|
+
]);
|
|
537
|
+
var messageDeltaPayload = z.object({ messageId: z.string(), text: z.string() });
|
|
538
|
+
var messageCompletedPayload = z.object({ messageId: z.string(), role: z.enum(["user", "assistant"]), text: z.string() });
|
|
539
|
+
var messageSteeredPayload = z.object({ text: z.string() });
|
|
540
|
+
var contextSnapshotPayload = z.object({
|
|
541
|
+
systemPrompt: z.string().max(1e5),
|
|
542
|
+
contextUpdate: z.string().max(1e5).optional(),
|
|
543
|
+
tools: z.array(z.object({ name: z.string(), description: z.string().max(2e3), parameters: z.unknown() })).max(120),
|
|
544
|
+
systemPromptTruncated: z.boolean().optional(),
|
|
545
|
+
contextUpdateTruncated: z.boolean().optional(),
|
|
546
|
+
toolsTruncated: z.boolean().optional()
|
|
547
|
+
});
|
|
548
|
+
var toolAttachmentRefSchema = z.object({
|
|
549
|
+
id: z.string().min(1).max(300),
|
|
550
|
+
name: z.string().min(1).max(500),
|
|
551
|
+
mimeType: z.string().min(1).max(100)
|
|
552
|
+
}).strict();
|
|
553
|
+
var toolAttachmentRefsSchema = z.array(toolAttachmentRefSchema).max(16);
|
|
554
|
+
var toolPayload = z.object({
|
|
555
|
+
toolCallId: z.string(),
|
|
556
|
+
name: z.string(),
|
|
557
|
+
summary: z.string().max(200).optional(),
|
|
558
|
+
input: z.string().max(24e3).optional(),
|
|
559
|
+
inputTruncated: z.boolean().optional(),
|
|
560
|
+
output: z.string().max(24e3).optional(),
|
|
561
|
+
outputTruncated: z.boolean().optional(),
|
|
562
|
+
ok: z.boolean().optional(),
|
|
563
|
+
/** On `tool.completed`: how long the call ran, and the size of what it returned to the model. */
|
|
564
|
+
durationMs: z.number().optional(),
|
|
565
|
+
resultChars: z.number().optional(),
|
|
566
|
+
/** Opaque attachment references. Content stays outside the run event stream. */
|
|
567
|
+
attachments: toolAttachmentRefsSchema.optional()
|
|
568
|
+
});
|
|
569
|
+
var retryStartedPayload = z.object({ attempt: z.number(), maxAttempts: z.number(), delayMs: z.number(), error: z.string().max(1e3).optional() });
|
|
570
|
+
var retryEndedPayload = z.object({ attempt: z.number(), success: z.boolean(), error: z.string().max(1e3).optional() });
|
|
571
|
+
var compactionStartedPayload = z.object({ reason: z.string() });
|
|
572
|
+
var compactionEndedPayload = z.object({
|
|
573
|
+
reason: z.string(),
|
|
574
|
+
aborted: z.boolean(),
|
|
575
|
+
willRetry: z.boolean().optional(),
|
|
576
|
+
error: z.string().max(1e3).optional(),
|
|
577
|
+
tokensBefore: z.number().optional(),
|
|
578
|
+
tokensAfter: z.number().optional(),
|
|
579
|
+
summary: z.string().max(24e3).optional(),
|
|
580
|
+
summaryTruncated: z.boolean().optional()
|
|
581
|
+
});
|
|
582
|
+
var approvalAutomaticSchema = z.enum(["always", "remembered", "scope"]);
|
|
583
|
+
var approvalRequestedPayload = z.object({
|
|
584
|
+
approvalId: z.string(),
|
|
585
|
+
target: z.string(),
|
|
586
|
+
action: z.string(),
|
|
587
|
+
screenshotRef: z.string().optional(),
|
|
588
|
+
operation: z.object({ tool: z.string(), args: z.unknown() }).optional(),
|
|
589
|
+
/** A tool the user may choose to always allow for this session. */
|
|
590
|
+
rememberable: z.boolean().optional(),
|
|
591
|
+
/** Set when the request was granted as it was made, so nobody is waiting on it. */
|
|
592
|
+
automatic: approvalAutomaticSchema.optional()
|
|
593
|
+
});
|
|
594
|
+
var approvalResolvedPayload = z.object({ approvalId: z.string(), decision: z.enum(["approved", "denied", "native"]), automatic: approvalAutomaticSchema.optional() });
|
|
595
|
+
var questionOptionSchema = z.object({ label: z.string().min(1), description: z.string().optional() });
|
|
596
|
+
var questionBatchFields = {
|
|
597
|
+
batchId: z.string().min(1).optional(),
|
|
598
|
+
batchIndex: z.number().int().min(1).max(5).optional(),
|
|
599
|
+
batchCount: z.number().int().min(1).max(5).optional()
|
|
600
|
+
};
|
|
601
|
+
var questionBatchRefinement = (value, context) => {
|
|
602
|
+
const fields = [value.batchId, value.batchIndex, value.batchCount];
|
|
603
|
+
if (fields.some((field) => field !== void 0) && fields.some((field) => field === void 0)) {
|
|
604
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "Question batch fields must be supplied together." });
|
|
605
|
+
}
|
|
606
|
+
if (value.batchIndex !== void 0 && value.batchCount !== void 0 && value.batchIndex > value.batchCount) {
|
|
607
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "Question batch index cannot exceed its count." });
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
var questionRequestedPayload = z.object({
|
|
611
|
+
questionId: z.string(),
|
|
612
|
+
question: z.string().min(1),
|
|
613
|
+
summary: z.string().min(1).max(100).optional(),
|
|
614
|
+
options: z.array(questionOptionSchema),
|
|
615
|
+
multiSelect: z.boolean(),
|
|
616
|
+
allowText: z.boolean(),
|
|
617
|
+
userOnly: z.boolean().optional(),
|
|
618
|
+
...questionBatchFields
|
|
619
|
+
}).superRefine(questionBatchRefinement);
|
|
620
|
+
var questionAnsweredPayload = z.object({
|
|
621
|
+
questionId: z.string(),
|
|
622
|
+
selected: z.array(z.string()),
|
|
623
|
+
text: z.string().optional(),
|
|
624
|
+
notes: z.record(z.string(), z.string().max(1e4)).optional(),
|
|
625
|
+
...questionBatchFields
|
|
626
|
+
}).superRefine(questionBatchRefinement);
|
|
627
|
+
var reactionAddedPayload = z.object({ emoji: z.string().min(1).refine((text3) => Buffer.byteLength(text3) <= 16 && !/[A-Za-z0-9\s]/.test(text3)) });
|
|
628
|
+
var agentMessagePayload = z.object({ direction: z.enum(["to", "from"]), agentId: z.string(), agentName: z.string(), text: z.string() });
|
|
629
|
+
var extensionNoticePayload = z.object({
|
|
630
|
+
kind: z.enum(["notify", "status", "widget", "title", "error"]),
|
|
631
|
+
level: z.enum(["info", "warning", "error"]).optional(),
|
|
632
|
+
key: z.string().optional(),
|
|
633
|
+
text: z.string(),
|
|
634
|
+
extension: z.string().optional()
|
|
635
|
+
});
|
|
636
|
+
var artifactPayload = z.object({ artifactId: z.string(), name: z.string(), file: z.unknown().optional() });
|
|
637
|
+
var fileChangedPayload = z.object({ path: z.string(), change: z.enum(["added", "modified", "deleted"]) });
|
|
638
|
+
var costSourceSchema = z.enum(["harness", "estimate", "mixed"]);
|
|
639
|
+
var usageRequestSchema = z.object({
|
|
640
|
+
/** Position in the run, from 1. */
|
|
641
|
+
index: z.number(),
|
|
642
|
+
responseId: z.string().optional(),
|
|
643
|
+
model: z.string().optional(),
|
|
644
|
+
provider: z.string().optional(),
|
|
645
|
+
inputTokens: z.number().optional(),
|
|
646
|
+
cacheReadTokens: z.number().optional(),
|
|
647
|
+
cacheWriteTokens: z.number().optional(),
|
|
648
|
+
outputTokens: z.number().optional(),
|
|
649
|
+
reasoningTokens: z.number().optional(),
|
|
650
|
+
costUsd: z.number().optional(),
|
|
651
|
+
costSource: z.enum(["harness", "estimate"]).optional(),
|
|
652
|
+
/** Separate provider-supplied summary; never generic private thinking content. */
|
|
653
|
+
reasoningSummary: z.string().max(4e3).optional(),
|
|
654
|
+
/** From sending the request to the end of the response, and to its first output. */
|
|
655
|
+
durationMs: z.number().optional(),
|
|
656
|
+
ttftMs: z.number().optional(),
|
|
657
|
+
stopReason: z.string().optional()
|
|
658
|
+
});
|
|
659
|
+
var usagePayloadSchema = z.object({
|
|
660
|
+
/** Input tokens that were not read from the provider's cache. */
|
|
661
|
+
inputTokens: z.number().optional(),
|
|
662
|
+
/** Includes reasoning tokens. */
|
|
663
|
+
outputTokens: z.number().optional(),
|
|
664
|
+
cacheReadTokens: z.number().optional(),
|
|
665
|
+
cacheWriteTokens: z.number().optional(),
|
|
666
|
+
reasoningTokens: z.number().optional(),
|
|
667
|
+
costUsd: z.number().optional(),
|
|
668
|
+
costSource: costSourceSchema.optional(),
|
|
669
|
+
/** Model requests made so far. */
|
|
670
|
+
requests: z.number().optional(),
|
|
671
|
+
/** Size of the latest request (not cumulative), and the model's limit. */
|
|
672
|
+
contextTokens: z.number().optional(),
|
|
673
|
+
contextWindow: z.number().optional(),
|
|
674
|
+
model: z.string().optional(),
|
|
675
|
+
provider: z.string().optional(),
|
|
676
|
+
/** The request this update reports. */
|
|
677
|
+
request: usageRequestSchema.optional()
|
|
678
|
+
});
|
|
679
|
+
var accountIssueCategorySchema = z.enum(["auth_expired", "rate_limited", "quota", "network", "unknown"]);
|
|
680
|
+
var accountLimitSchema = z.object({
|
|
681
|
+
id: z.string(),
|
|
682
|
+
label: z.string(),
|
|
683
|
+
usedPercent: z.number().nullable().optional(),
|
|
684
|
+
resetsAt: z.string().nullable().optional(),
|
|
685
|
+
windowMinutes: z.number().nullable().optional(),
|
|
686
|
+
status: z.string().nullable().optional()
|
|
687
|
+
});
|
|
688
|
+
var accountStatusPayload = z.object({
|
|
689
|
+
accountId: z.string(),
|
|
690
|
+
category: accountIssueCategorySchema.optional(),
|
|
691
|
+
message: z.string().optional(),
|
|
692
|
+
resetsAt: z.string().optional(),
|
|
693
|
+
plan: z.string().optional(),
|
|
694
|
+
limits: z.array(accountLimitSchema).optional()
|
|
695
|
+
});
|
|
696
|
+
var statusPayload = z.object({ status: runStatusSchema, reason: z.string().optional() });
|
|
697
|
+
var failedPayload = z.object({ code: z.string(), message: z.string() });
|
|
698
|
+
var emptyPayload = z.object({}).passthrough();
|
|
699
|
+
var runEventPayloadSchemas = {
|
|
700
|
+
started: emptyPayload,
|
|
701
|
+
"context.snapshot": contextSnapshotPayload,
|
|
702
|
+
"message.delta": messageDeltaPayload,
|
|
703
|
+
"message.completed": messageCompletedPayload,
|
|
704
|
+
"message.steered": messageSteeredPayload,
|
|
705
|
+
"tool.started": toolPayload,
|
|
706
|
+
"tool.completed": toolPayload,
|
|
707
|
+
"retry.started": retryStartedPayload,
|
|
708
|
+
"retry.ended": retryEndedPayload,
|
|
709
|
+
"compaction.started": compactionStartedPayload,
|
|
710
|
+
"compaction.ended": compactionEndedPayload,
|
|
711
|
+
"approval.requested": approvalRequestedPayload,
|
|
712
|
+
"approval.resolved": approvalResolvedPayload,
|
|
713
|
+
"question.requested": questionRequestedPayload,
|
|
714
|
+
"question.answered": questionAnsweredPayload,
|
|
715
|
+
"extension.notice": extensionNoticePayload,
|
|
716
|
+
"reaction.added": reactionAddedPayload,
|
|
717
|
+
"agent.message": agentMessagePayload,
|
|
718
|
+
"artifact.added": artifactPayload,
|
|
719
|
+
"file.changed": fileChangedPayload,
|
|
720
|
+
"usage.updated": usagePayloadSchema,
|
|
721
|
+
"account.status": accountStatusPayload,
|
|
722
|
+
"status.changed": statusPayload,
|
|
723
|
+
failed: failedPayload,
|
|
724
|
+
completed: emptyPayload
|
|
725
|
+
};
|
|
726
|
+
var runEventSchema = z.object({
|
|
727
|
+
id: z.string(),
|
|
728
|
+
runId: z.string(),
|
|
729
|
+
sessionId: z.string(),
|
|
730
|
+
sequence: z.number().int().nonnegative(),
|
|
731
|
+
timestamp: z.string(),
|
|
732
|
+
type: runEventTypeSchema,
|
|
733
|
+
payload: z.unknown(),
|
|
734
|
+
/** The native event this one was derived from, kept as evidence. */
|
|
735
|
+
native: z.unknown().optional(),
|
|
736
|
+
/** Set when consecutive text deltas were merged for relay; covers sequences coalescedFrom..sequence. */
|
|
737
|
+
coalescedFrom: z.number().int().nonnegative().optional()
|
|
738
|
+
}).superRefine((event, context) => {
|
|
739
|
+
if (!runEventPayloadSchemas[event.type].safeParse(event.payload).success) {
|
|
740
|
+
context.addIssue({ code: "custom", path: ["payload"], message: `Invalid payload for ${event.type}` });
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
var text = z.string().nullish().transform((value) => value ?? null);
|
|
744
|
+
var runRecordSchema = z.object({
|
|
745
|
+
resumedFromRunId: text,
|
|
746
|
+
parentRunId: text,
|
|
747
|
+
relation: text,
|
|
748
|
+
terminalReason: text,
|
|
749
|
+
id: z.string(),
|
|
750
|
+
projectId: text,
|
|
751
|
+
mode: z.string(),
|
|
752
|
+
harness: z.string(),
|
|
753
|
+
sessionType: z.string(),
|
|
754
|
+
sessionId: z.string(),
|
|
755
|
+
nativeSessionId: text,
|
|
756
|
+
workingDirectory: text,
|
|
757
|
+
worktreeId: text,
|
|
758
|
+
goal: text,
|
|
759
|
+
attachmentCount: z.number().int().nonnegative().default(0),
|
|
760
|
+
startedAt: z.string(),
|
|
761
|
+
endedAt: text,
|
|
762
|
+
status: z.string(),
|
|
763
|
+
exitCode: z.number().int().nullish().transform((value) => value ?? null),
|
|
764
|
+
handoffId: text,
|
|
765
|
+
accountId: text,
|
|
766
|
+
agentId: text,
|
|
767
|
+
trigger: text
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// src/core/index.ts
|
|
771
|
+
import { DatabaseSync } from "node:sqlite";
|
|
772
|
+
|
|
773
|
+
// src/sessions/subagents.ts
|
|
774
|
+
var SUBAGENT_RECEIPT = "xingularSubagents";
|
|
775
|
+
var SUBAGENT_OWNER = "xingular-subagent-owner";
|
|
776
|
+
var SUBAGENT_RESULTS = "xingular-subagent-results";
|
|
777
|
+
function subagentHistory(entries) {
|
|
778
|
+
const owners = [];
|
|
779
|
+
const receipts = [];
|
|
780
|
+
for (const raw of entries) {
|
|
781
|
+
if (!raw || typeof raw !== "object") continue;
|
|
782
|
+
const entry = raw;
|
|
783
|
+
if (entry.type === "custom" && entry.customType === SUBAGENT_OWNER && typeof entry.data?.runId === "string") owners.push(entry.data.runId);
|
|
784
|
+
const message2 = entry.type === "message" ? entry.message : entry;
|
|
785
|
+
const receipt = message2?.details?.[SUBAGENT_RECEIPT];
|
|
786
|
+
if (!receipt || receipt.version !== 1 || typeof receipt.ownerRunId !== "string") continue;
|
|
787
|
+
const strings = (value) => Array.isArray(value) && value.every((id) => typeof id === "string");
|
|
788
|
+
if (receipt.spawned !== void 0 && !strings(receipt.spawned) || receipt.delivered !== void 0 && !strings(receipt.delivered)) continue;
|
|
789
|
+
receipts.push(receipt);
|
|
790
|
+
}
|
|
791
|
+
return { owners, receipts };
|
|
792
|
+
}
|
|
500
793
|
|
|
501
794
|
// src/tools/askUser.ts
|
|
502
795
|
var ASK_USER = "ask_user";
|
|
503
796
|
var MAX_OPTIONS = 6;
|
|
504
|
-
var
|
|
797
|
+
var text2 = (value) => typeof value === "string" ? value.trim() : "";
|
|
505
798
|
function describeAnswer(answer) {
|
|
506
799
|
const parts = [];
|
|
507
800
|
if (answer.selected.length) parts.push(`The user chose: ${answer.selected.join(", ")}.`);
|
|
@@ -509,7 +802,7 @@ function describeAnswer(answer) {
|
|
|
509
802
|
for (const [choice, note] of Object.entries(answer.notes ?? {})) parts.push(`Their note for ${choice}: ${note}`);
|
|
510
803
|
return parts.join(" ") || "The user did not answer.";
|
|
511
804
|
}
|
|
512
|
-
function askUserTool(
|
|
805
|
+
function askUserTool(run2) {
|
|
513
806
|
return {
|
|
514
807
|
name: ASK_USER,
|
|
515
808
|
label: "Ask the user",
|
|
@@ -531,15 +824,15 @@ function askUserTool(run) {
|
|
|
531
824
|
if (!Array.isArray(input.questions) || input.questions.length < 1 || input.questions.length > 5) return "Ask between one and five questions.";
|
|
532
825
|
const questions = input.questions.map((raw) => {
|
|
533
826
|
const item = raw;
|
|
534
|
-
const summary =
|
|
535
|
-
const question =
|
|
536
|
-
const options = (Array.isArray(item.options) ? item.options : []).map((option) => ({ label:
|
|
827
|
+
const summary = text2(item.summary);
|
|
828
|
+
const question = text2(item.question);
|
|
829
|
+
const options = (Array.isArray(item.options) ? item.options : []).map((option) => ({ label: text2(option.label), description: text2(option.description) })).filter((option) => option.label).slice(0, MAX_OPTIONS);
|
|
537
830
|
return { summary, question, options, multiSelect: options.length > 1 && item.multiSelect === true, allowText: true };
|
|
538
831
|
});
|
|
539
832
|
if (questions.some((question) => !question.summary || question.summary.length > 100)) return "Each question needs a short summary of no more than 100 characters.";
|
|
540
833
|
if (questions.some((question) => !question.question)) return "Every question needs a question sentence.";
|
|
541
834
|
if (questions.some((question) => question.options.some((option) => !option.description))) return "Every choice needs a short description. Aim for 25 words or fewer.";
|
|
542
|
-
const answers = await
|
|
835
|
+
const answers = await run2.requestAnswers(questions);
|
|
543
836
|
return questions.map((question, index) => `${question.summary}: ${describeAnswer(answers[index])}`).join("\n");
|
|
544
837
|
}
|
|
545
838
|
};
|
|
@@ -551,9 +844,24 @@ function resultSize(result) {
|
|
|
551
844
|
if (!Array.isArray(content)) return typeof result === "string" ? result.length : null;
|
|
552
845
|
return content.reduce((total, part) => total + (part?.type === "text" && typeof part.text === "string" ? part.text.length : 0), 0);
|
|
553
846
|
}
|
|
847
|
+
async function withParentCancellation(signal, work) {
|
|
848
|
+
const stop = () => {
|
|
849
|
+
void controlCall("terminal.interrupt").catch(() => {
|
|
850
|
+
});
|
|
851
|
+
};
|
|
852
|
+
if (signal?.aborted) stop();
|
|
853
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
854
|
+
try {
|
|
855
|
+
return await work();
|
|
856
|
+
} finally {
|
|
857
|
+
signal?.removeEventListener("abort", stop);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
554
860
|
async function piExtension_default(pi) {
|
|
555
861
|
await terminalCall("startup", { phase: "Pi extension loaded" });
|
|
556
862
|
let context = null;
|
|
863
|
+
let nativeSubagents = false;
|
|
864
|
+
let subagentParent = false;
|
|
557
865
|
let timer = null;
|
|
558
866
|
let polling = false;
|
|
559
867
|
let failure;
|
|
@@ -624,6 +932,18 @@ async function piExtension_default(pi) {
|
|
|
624
932
|
}
|
|
625
933
|
});
|
|
626
934
|
}
|
|
935
|
+
const native = await controlCall("terminal.subagents.tools");
|
|
936
|
+
nativeSubagents = native.tools.length > 0;
|
|
937
|
+
for (const tool of native.tools) pi.registerTool({
|
|
938
|
+
name: tool.name,
|
|
939
|
+
label: tool.label,
|
|
940
|
+
description: tool.description,
|
|
941
|
+
parameters: Type.Unsafe(tool.parameters),
|
|
942
|
+
execute: async (_id, input, signal) => {
|
|
943
|
+
const result = await withParentCancellation(signal, () => controlCall("terminal.subagents.call", { name: tool.name, input }, 2147483647));
|
|
944
|
+
return { content: [{ type: "text", text: result.text }], details: result.details ?? {} };
|
|
945
|
+
}
|
|
946
|
+
});
|
|
627
947
|
} catch (error) {
|
|
628
948
|
await terminalCall("startupFailed", { reason: `${stage} failed: ${error instanceof Error ? error.message : "Unavailable"}` }).catch(() => {
|
|
629
949
|
});
|
|
@@ -656,13 +976,26 @@ async function piExtension_default(pi) {
|
|
|
656
976
|
usage = emptyUsage();
|
|
657
977
|
reasoningTokens = void 0;
|
|
658
978
|
costReported = false;
|
|
659
|
-
await terminalCall("begin", { text: event.messageType === "user" ? event.prompt : "" });
|
|
979
|
+
const begun = await terminalCall("begin", { text: event.messageType === "user" ? event.prompt : "" });
|
|
980
|
+
if (nativeSubagents) {
|
|
981
|
+
pi.appendEntry(SUBAGENT_OWNER, { runId: begun.runId });
|
|
982
|
+
await controlCall("terminal.subagents.register", { tools: pi.getActiveTools(), model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null, effort: pi.getThinkingLevel(), history: subagentHistory(ctx.sessionManager.getBranch()) });
|
|
983
|
+
subagentParent = true;
|
|
984
|
+
}
|
|
660
985
|
});
|
|
986
|
+
const deliverChildren = async (event, ctx, settle) => {
|
|
987
|
+
if (!nativeSubagents || settle && event.outcome !== "completed" || ctx.signal?.aborted) return;
|
|
988
|
+
const result = await withParentCancellation(ctx.signal, () => controlCall("terminal.subagents.delivery", { history: subagentHistory([...ctx.sessionManager.getBranch(), ...event.entries]), settle }, 2147483647));
|
|
989
|
+
if (!result) return;
|
|
990
|
+
return { entries: [...event.entries, { type: "custom_message", customType: SUBAGENT_RESULTS, content: result.text, display: false, details: { [SUBAGENT_RECEIPT]: result.receipt } }], continue: true };
|
|
991
|
+
};
|
|
992
|
+
pi.on("turn_end", (event, ctx) => deliverChildren(event, ctx, false));
|
|
993
|
+
pi.on("agent_before_settle", (event, ctx) => deliverChildren(event, ctx, true));
|
|
661
994
|
pi.on("message_end", async (event) => {
|
|
662
995
|
if (event.message.role !== "assistant") return;
|
|
663
996
|
if (event.message.stopReason === "error" || event.message.stopReason === "aborted") failure = event.message.errorMessage ?? `Pi stopped: ${event.message.stopReason}`;
|
|
664
|
-
const
|
|
665
|
-
if (
|
|
997
|
+
const text3 = event.message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
|
|
998
|
+
if (text3) await terminalCall("message", { text: text3 });
|
|
666
999
|
if (event.message.usage) {
|
|
667
1000
|
const reported = event.message.usage;
|
|
668
1001
|
usage.inputTokens += reported.input ?? 0;
|
|
@@ -707,9 +1040,9 @@ async function piExtension_default(pi) {
|
|
|
707
1040
|
});
|
|
708
1041
|
pi.on("tool_call", async (event) => {
|
|
709
1042
|
try {
|
|
710
|
-
const policy = await
|
|
1043
|
+
const policy = await controlCall("terminal.policy", { tool: event.toolName, args: event.input });
|
|
711
1044
|
if (policy.requiresApproval) {
|
|
712
|
-
const approval = await
|
|
1045
|
+
const approval = await controlCall("terminal.approval", { requestId: event.toolCallId, tool: event.toolName, args: event.input });
|
|
713
1046
|
if (approval.decision === "approved") return;
|
|
714
1047
|
return { block: true, reason: "The user denied this action. Ask how they would like to proceed." };
|
|
715
1048
|
}
|
|
@@ -732,8 +1065,16 @@ async function piExtension_default(pi) {
|
|
|
732
1065
|
fileTools.delete(event.toolCallId);
|
|
733
1066
|
if (!event.isError && path) await terminalCall("file", { path, change: "modified" });
|
|
734
1067
|
});
|
|
735
|
-
pi.on("agent_settled", async () => {
|
|
736
|
-
|
|
1068
|
+
pi.on("agent_settled", async (event, ctx) => {
|
|
1069
|
+
if (subagentParent && !event.aborted && !failure) {
|
|
1070
|
+
try {
|
|
1071
|
+
await controlCall("terminal.subagents.verify", { history: subagentHistory(ctx.sessionManager.getBranch()) }, 2147483647);
|
|
1072
|
+
} catch (error) {
|
|
1073
|
+
failure = error instanceof Error ? error.message : String(error);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
subagentParent = false;
|
|
1077
|
+
await terminalCall("settled", { error: failure, aborted: event.aborted });
|
|
737
1078
|
});
|
|
738
1079
|
pi.on("user_bash", async () => {
|
|
739
1080
|
throw new Error("Use the bash tool so this action can be checked.");
|
package/docs/README.md
CHANGED
|
@@ -5,6 +5,9 @@ Current implementation: [Harness terminal frontend](terminal-tui.md). The packag
|
|
|
5
5
|
|
|
6
6
|
| ID | Record | Type | Status | Related |
|
|
7
7
|
|---|---|---|---|---|
|
|
8
|
+
| ADR-0004 | [Fullscreen terminal layout with a fixed dock](decisions/ADR-0004-fullscreen-terminal-layout.md) | Decision | Accepted; working tree implementation | ADR-0001, [Terminal architecture and evidence](terminal-tui.md) |
|
|
9
|
+
| ADR-0003 | [Shared asynchronous subagents with native-history delivery receipts](decisions/ADR-0003-shared-asynchronous-subagents.md) | Decision | Accepted; working tree implementation | ADR-0002, [Embedding API](../README.md) |
|
|
10
|
+
| ADR-0002 | [Native subagent lifecycle in Harness sessions](decisions/ADR-0002-native-subagent-lifecycle.md) | Decision | Superseded by ADR-0003 | [Embedding API](../README.md) |
|
|
8
11
|
| ADR-0001 | [Own the terminal frontend and keep Pi as the session runtime](decisions/ADR-0001-owned-terminal.md) | Decision | Accepted; working tree implementation | [Terminal architecture and evidence](terminal-tui.md) |
|
|
9
12
|
|
|
10
13
|
Living reference pages describe checked source behavior. Accepted decisions record the choice;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: ADR-0002
|
|
3
|
+
title: Native subagent lifecycle in Harness sessions
|
|
4
|
+
status: superseded
|
|
5
|
+
superseded_by: ADR-0003
|
|
6
|
+
created: 2026-10-18
|
|
7
|
+
updated: 2026-10-18
|
|
8
|
+
---
|
|
9
|
+
# ADR-0002 — Native subagent lifecycle in Harness sessions
|
|
10
|
+
|
|
11
|
+
Superseded by [ADR-0003](ADR-0003-shared-asynchronous-subagents.md), which expands the native
|
|
12
|
+
service to asynchronous execution, reliable delivery, and terminal parity. The earlier decision
|
|
13
|
+
and its rationale are retained below.
|
|
14
|
+
|
|
15
|
+
## Context
|
|
16
|
+
|
|
17
|
+
Subagents already used Harness sessions, capability snapshots and run lineage, but their tool
|
|
18
|
+
owned launching and polled run files for completion. A host inferred ownership from session
|
|
19
|
+
names, coupling it to an internal identifier format.
|
|
20
|
+
|
|
21
|
+
## Decision
|
|
22
|
+
|
|
23
|
+
`Sessions.spawnSubagent` owns child creation. A live parent's captured launch supplies caller,
|
|
24
|
+
account, model selection, folder, policy context, prompt and capability ceiling. The host or
|
|
25
|
+
model supplies only a complete brief and an optional tool subset. Current capability availability
|
|
26
|
+
and the captured nesting limit further restrict each child. Empty tool subsets stay empty;
|
|
27
|
+
spawning permission is never added back after narrowing. Children start with fresh history,
|
|
28
|
+
without inherited image attachments or request preamble. Existing tool gating applies.
|
|
29
|
+
|
|
30
|
+
The model tool is an adapter to the same native API exposed over RPC. `RunRegistry.wait` resolves
|
|
31
|
+
on settlement, including failed startup and interruption, without polling or per-call listeners.
|
|
32
|
+
The Harness Subagents plugin attributes the model-visible tool. Opt-in remains a session option;
|
|
33
|
+
`{}` selects the Harness default depth of one. Application organization delegation remains a host
|
|
34
|
+
concern. Consumers resolve ownership from run lineage rather than parsing session IDs.
|
|
35
|
+
|
|
36
|
+
## Alternatives and consequences
|
|
37
|
+
|
|
38
|
+
Keeping orchestration in the tool would leave hosts without the same lifecycle contract.
|
|
39
|
+
Moving application organizations into Harness would violate the embedding boundary. A new
|
|
40
|
+
background job manager would duplicate existing run tracking, approvals and interruption.
|
|
41
|
+
The selected change reuses these layers and preserves the synchronous model tool behavior.
|
|
42
|
+
The native API starts children only for live parents in the same Harness instance; it does not
|
|
43
|
+
resume a stopped parent's launch or implement cross-process child management.
|
|
44
|
+
|
|
45
|
+
## Evidence
|
|
46
|
+
|
|
47
|
+
Regression coverage is in `src/sessions/sessions.test.ts` and `src/server/server.test.ts`.
|
|
48
|
+
Application host and run-family tests cover nested lineage with opaque session IDs. Verification
|
|
49
|
+
results belong to the implementation handoff; no release or live-provider behavior is claimed.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: ADR-0003
|
|
3
|
+
title: Shared asynchronous subagents with native-history delivery receipts
|
|
4
|
+
status: accepted
|
|
5
|
+
created: 2026-10-18
|
|
6
|
+
updated: 2026-10-18
|
|
7
|
+
supersedes: ADR-0002
|
|
8
|
+
---
|
|
9
|
+
# ADR-0003 — Shared asynchronous subagents with native-history delivery receipts
|
|
10
|
+
|
|
11
|
+
## Context and decision
|
|
12
|
+
|
|
13
|
+
The owner selected reliable delivery, asynchronous spawn/wait, and one SDK/terminal/App API after
|
|
14
|
+
studying OMP. They chose attached children joined before the parent finishes, async spawning by
|
|
15
|
+
default, availability for every interactive caller within its capability ceiling, and recovery
|
|
16
|
+
only when the user explicitly resumes. This supersedes the blocking tool contract in ADR-0002.
|
|
17
|
+
|
|
18
|
+
`harness.subagents` owns spawning, selective waits and results. SDK and terminal adapters register
|
|
19
|
+
live, frozen parent context with it; no model argument supplies caller, account or replacement
|
|
20
|
+
launch metadata. The terminal adapter uses its private control key. Children use normal Harness
|
|
21
|
+
sessions, isolated accounts, fresh history, existing gates and capability intersection. The old
|
|
22
|
+
`sessions.spawnSubagent` entry point forwards to the service for compatibility.
|
|
23
|
+
|
|
24
|
+
A parent reserves each child in `runs/<parent>/subagents.json` through the run store before startup.
|
|
25
|
+
A native branch owner marker is persisted before a real parent accepts spawns. Results are delivered
|
|
26
|
+
at turn boundaries or through explicit blocking tools. Native messages and tool results carry typed
|
|
27
|
+
receipts; only receipts on the selected branch acknowledge delivery. Constructing a delivery draft
|
|
28
|
+
never acknowledges it. Large batches acknowledge only the included children. The final settlement
|
|
29
|
+
hook joins children and injects remaining results; an adapter verification guard refuses successful
|
|
30
|
+
completion if persisted native history still lacks a result. The stable system prefix is unchanged.
|
|
31
|
+
|
|
32
|
+
On explicit resume, matching owner markers, caller/account/folder/native identity and run lineage
|
|
33
|
+
restore undelivered results. A startup reservation that never opened becomes an interrupted run.
|
|
34
|
+
Existing startup recovery interrupts orphaned active runs. Neither recovery path restarts model
|
|
35
|
+
work. Cancellation settles approvals and questions before waiting for SDK abort, preventing blocked
|
|
36
|
+
child interactions from deadlocking parent shutdown.
|
|
37
|
+
|
|
38
|
+
Interactive sessions enable the tools by default, `null` disables them, and explicit capability
|
|
39
|
+
sets can exclude them. Unattended callers remain off unless the host explicitly enables them.
|
|
40
|
+
Default nesting remains one, with a maximum of three. There is no new scheduler, reusable-worker
|
|
41
|
+
lifecycle, model routing, workspace isolation system, concurrency cap or family budget here.
|
|
42
|
+
|
|
43
|
+
## Alternatives and consequences
|
|
44
|
+
|
|
45
|
+
A tool-owned launcher leaves terminal and SDK behavior divergent. An in-memory delivery flag loses
|
|
46
|
+
results across restart; searching prose for run IDs confuses mentions with delivery. Native-history
|
|
47
|
+
receipts identify persisted context without adding a second acknowledgement store. Run records
|
|
48
|
+
remain the outcome source of truth, while the parent file preserves startup reservations.
|
|
49
|
+
|
|
50
|
+
The App owns organization delegation and resolves native ancestry for thread routing, tools,
|
|
51
|
+
isolation and connections. Its existing work-budget accounting includes native descendants. Harness
|
|
52
|
+
learns no organizational role or application concept. Native completion records execution outcome;
|
|
53
|
+
it does not certify the child's findings or artifacts.
|
|
54
|
+
|
|
55
|
+
## Evidence and limits
|
|
56
|
+
|
|
57
|
+
`subagentService.test.ts` covers selective waits, foreign handles, cancellation, bounded batches and
|
|
58
|
+
recovery windows. Real scripted Pi tests in `conversation.test.ts` cover async delivery, inline
|
|
59
|
+
receipts and interruption during a child question. `runtime.socket.test.ts` exercises the real
|
|
60
|
+
terminal socket, private controls, native execution and child decision routing. RPC and App host
|
|
61
|
+
regressions cover protocol shapes and ancestry. Tests use disposable storage without external
|
|
62
|
+
model requests. These changes shipped in `@xingular/harness` 2.0.0 (commit `44471bc`); release does not imply live-provider acceptance.
|
|
63
|
+
|
|
64
|
+
Verification on 2026-10-18: Harness typecheck, 455 tests (two skipped), unchanged golden fixtures,
|
|
65
|
+
build and standalone proof passed. App typecheck, layer/UI checks, sidecar build, 30 sidecar tests
|
|
66
|
+
and 217 Rust tests passed (one ignored). The renderer suite passed 491 tests and retained its
|
|
67
|
+
pre-existing `PendingRequestDock.test.ts` heading-text assertion failure. No live account or
|
|
68
|
+
user-data-root testing was performed.
|
|
69
|
+
|
|
70
|
+
Release verification on 2026-10-18: npm 10 clean install, typecheck, 515 tests (two skipped), build and standalone proof passed. GitHub CI passed on Ubuntu and macOS, and the npm publish workflow released version 2.0.0 with provenance. No live provider requests were made during release verification.
|