@xingular/harness 1.3.1-unstable.2 → 1.3.1-unstable.4

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.
Files changed (125) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +1 -1
  3. package/dist/cli.js +3680 -1947
  4. package/dist/sdk/cli.js +2 -0
  5. package/dist/sdk/commands/registry.d.ts +15 -0
  6. package/dist/sdk/commands/registry.js +2 -0
  7. package/dist/sdk/computer/screenshot.d.ts +32 -0
  8. package/dist/sdk/computer/screenshot.js +35 -0
  9. package/dist/sdk/context/attribution.d.ts +18 -0
  10. package/dist/sdk/context/attribution.js +295 -0
  11. package/dist/sdk/context/types.d.ts +94 -0
  12. package/dist/sdk/context/types.js +10 -0
  13. package/dist/sdk/index.d.ts +2 -1
  14. package/dist/sdk/index.js +2 -1
  15. package/dist/sdk/plugins/builtin.js +1 -0
  16. package/dist/sdk/prompt/compile.d.ts +42 -2
  17. package/dist/sdk/prompt/compile.js +87 -15
  18. package/dist/sdk/remote/addresses.d.ts +19 -0
  19. package/dist/sdk/remote/addresses.js +52 -0
  20. package/dist/sdk/remote/pairing.d.ts +59 -0
  21. package/dist/sdk/remote/pairing.js +160 -0
  22. package/dist/sdk/remote/seal.d.ts +28 -0
  23. package/dist/sdk/remote/seal.js +84 -0
  24. package/dist/sdk/remote/server.d.ts +28 -0
  25. package/dist/sdk/remote/server.js +158 -0
  26. package/dist/sdk/remote/service.d.ts +125 -0
  27. package/dist/sdk/remote/service.js +330 -0
  28. package/dist/sdk/remote/webClient.d.ts +4 -0
  29. package/dist/sdk/remote/webClient.js +444 -0
  30. package/dist/sdk/server/methods.js +3 -0
  31. package/dist/sdk/sessions/contracts.d.ts +4 -0
  32. package/dist/sdk/sessions/contracts.js +2 -0
  33. package/dist/sdk/sessions/planMode.d.ts +26 -1
  34. package/dist/sdk/sessions/planMode.js +48 -1
  35. package/dist/sdk/sessions/runner.d.ts +12 -0
  36. package/dist/sdk/sessions/runner.js +34 -2
  37. package/dist/sdk/sessions/sessions.js +1 -0
  38. package/dist/sdk/sessions/subagentService.d.ts +11 -1
  39. package/dist/sdk/sessions/subagentService.js +120 -38
  40. package/dist/sdk/sessions/subagents.d.ts +31 -0
  41. package/dist/sdk/sessions/subagents.js +45 -0
  42. package/dist/sdk/sessions/workSlice.d.ts +5 -9
  43. package/dist/sdk/sessions/workSlice.js +8 -149
  44. package/dist/sdk/terminal/client.d.ts +3 -1
  45. package/dist/sdk/terminal/client.js +15 -6
  46. package/dist/sdk/terminal/control.d.ts +1 -1
  47. package/dist/sdk/terminal/control.js +2 -2
  48. package/dist/sdk/terminal/controller.d.ts +22 -0
  49. package/dist/sdk/terminal/controller.js +104 -13
  50. package/dist/sdk/terminal/piExtension.js +106 -4
  51. package/dist/sdk/terminal/stallWatch.d.ts +13 -0
  52. package/dist/sdk/terminal/stallWatch.js +16 -0
  53. package/dist/sdk/terminal/standalone.d.ts +2 -0
  54. package/dist/sdk/terminal/standalone.js +9 -2
  55. package/dist/sdk/terminal/terminals.d.ts +14 -0
  56. package/dist/sdk/terminal/terminals.js +98 -2
  57. package/dist/sdk/terminal/tui/commandPages.d.ts +4 -1
  58. package/dist/sdk/terminal/tui/commandPages.js +12 -0
  59. package/dist/sdk/terminal/tui/commands.d.ts +3 -2
  60. package/dist/sdk/terminal/tui/commands.js +19 -5
  61. package/dist/sdk/terminal/tui/dialogs.d.ts +3 -2
  62. package/dist/sdk/terminal/tui/dialogs.js +4 -3
  63. package/dist/sdk/terminal/tui/keybindings.d.ts +1 -1
  64. package/dist/sdk/terminal/tui/keybindings.js +1 -1
  65. package/dist/sdk/terminal/tui/markdown.d.ts +16 -0
  66. package/dist/sdk/terminal/tui/markdown.js +55 -0
  67. package/dist/sdk/terminal/tui/modeChoices.d.ts +3 -2
  68. package/dist/sdk/terminal/tui/modeChoices.js +4 -4
  69. package/dist/sdk/terminal/tui/mouseReport.d.ts +30 -0
  70. package/dist/sdk/terminal/tui/mouseReport.js +80 -0
  71. package/dist/sdk/terminal/tui/pages/annotatePage.js +3 -2
  72. package/dist/sdk/terminal/tui/pages/browserPage.d.ts +1 -1
  73. package/dist/sdk/terminal/tui/pages/contextPage.d.ts +62 -0
  74. package/dist/sdk/terminal/tui/pages/contextPage.js +484 -0
  75. package/dist/sdk/terminal/tui/pages/remoteControlPage.d.ts +32 -0
  76. package/dist/sdk/terminal/tui/pages/remoteControlPage.js +170 -0
  77. package/dist/sdk/terminal/tui/pages/trajectoryPage.js +5 -4
  78. package/dist/sdk/terminal/tui/qr.d.ts +4 -0
  79. package/dist/sdk/terminal/tui/qr.js +35 -0
  80. package/dist/sdk/terminal/tui/runtime.js +8 -2
  81. package/dist/sdk/terminal/tui/search.js +3 -2
  82. package/dist/sdk/terminal/tui/sideQuestion.d.ts +1 -0
  83. package/dist/sdk/terminal/tui/sideQuestion.js +8 -3
  84. package/dist/sdk/terminal/tui/skillInvocation.d.ts +21 -2
  85. package/dist/sdk/terminal/tui/skillInvocation.js +31 -11
  86. package/dist/sdk/terminal/tui/status.d.ts +6 -0
  87. package/dist/sdk/terminal/tui/status.js +15 -1
  88. package/dist/sdk/terminal/tui/theme.d.ts +9 -0
  89. package/dist/sdk/terminal/tui/theme.js +9 -0
  90. package/dist/sdk/tools/defaultSkills.d.ts +19 -0
  91. package/dist/sdk/tools/defaultSkills.js +27 -1
  92. package/dist/sdk/tools/dispatch.js +8 -9
  93. package/dist/sdk/tools/spawnSubagent.d.ts +1 -1
  94. package/dist/sdk/tools/spawnSubagent.js +4 -2
  95. package/dist/terminal-opentui.js +1607 -492
  96. package/dist/terminal-pi.js +256 -144
  97. package/docs/README.md +1 -0
  98. package/docs/architecture/02-sessions-and-runs.md +55 -22
  99. package/docs/architecture/04-prompt.md +23 -4
  100. package/docs/architecture/07-tools-and-gate.md +4 -2
  101. package/docs/architecture/10-server-protocol.md +24 -0
  102. package/docs/architecture/11-terminal.md +52 -10
  103. package/docs/architecture/12-storage.md +5 -2
  104. package/docs/architecture/13-world-tools.md +17 -3
  105. package/docs/architecture/14-surfaces-and-parity.md +7 -3
  106. package/docs/architecture/15-remote-control.md +118 -0
  107. package/docs/architecture/README.md +3 -0
  108. package/docs/architecture/proposals/PROP-0049-context-attribution.md +409 -0
  109. package/docs/architecture/proposals/README.md +2 -1
  110. package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +4 -0
  111. package/docs/decisions/ADR-0048-required-skills-from-a-message.md +42 -0
  112. package/docs/decisions/ADR-0050-approved-window-screenshots.md +19 -0
  113. package/docs/decisions/ADR-0051-terminal-decision-wait.md +10 -0
  114. package/docs/decisions/ADR-0052-per-profile-subagent-budgets.md +25 -0
  115. package/docs/decisions/ADR-0053-no-repeated-work-guard.md +45 -0
  116. package/docs/decisions/ADR-0054-terminal-reply-heartbeat-and-stall.md +52 -0
  117. package/docs/decisions/ADR-0055-plan-output-repair.md +51 -0
  118. package/docs/decisions/ADR-0056-remote-control.md +53 -0
  119. package/docs/decisions/ADR-0057-structured-plan-outcomes.md +39 -0
  120. package/docs/decisions/ADR-0058-ask-user-only-plan-clarification.md +36 -0
  121. package/docs/parity.md +14 -10
  122. package/docs/releases/1.3.1-unstable.3.md +54 -0
  123. package/docs/releases/1.3.1-unstable.4.md +56 -0
  124. package/docs/terminal-tui.md +43 -18
  125. package/package.json +3 -1
package/dist/sdk/cli.js CHANGED
@@ -52,6 +52,8 @@ Standalone terminal options:
52
52
  Start in a new managed worktree of this repository, or in
53
53
  the named one
54
54
  --variant <scope> Run on this variant scope's head (default: interactive profile scope)
55
+ --remote-control, --rc
56
+ Turn on /remote-control at start, to continue from a phone or iPad
55
57
  `;
56
58
  function flags(argv) {
57
59
  const values = {};
@@ -69,6 +69,14 @@ export declare const CORE_COMMANDS: readonly [{
69
69
  readonly group: "Session";
70
70
  readonly kind: "session";
71
71
  readonly methods: readonly ["sessions.tree", "sessions.fork"];
72
+ }, {
73
+ readonly name: "remote-control";
74
+ readonly aliases: readonly ["rc"];
75
+ readonly args: "[off|tailscale|lan]";
76
+ readonly description: "Continue this session from your phone or iPad";
77
+ readonly group: "Session";
78
+ readonly kind: "page";
79
+ readonly methods: readonly ["terminal.remoteControl.start", "terminal.remoteControl.stop", "terminal.remoteControl.status"];
72
80
  }, {
73
81
  readonly name: "clone";
74
82
  readonly description: "Clone this branch into a new session";
@@ -228,6 +236,13 @@ export declare const CORE_COMMANDS: readonly [{
228
236
  readonly description: "Harness diagnostics and this conversation's run events";
229
237
  readonly group: "Inspect";
230
238
  readonly kind: "page";
239
+ }, {
240
+ readonly name: "context";
241
+ readonly args: "[growth]";
242
+ readonly description: "What the latest request was made of, and where the rest of the window went";
243
+ readonly group: "Inspect";
244
+ readonly kind: "page";
245
+ readonly methods: readonly ["context.attribution"];
231
246
  }, {
232
247
  readonly name: "hotkeys";
233
248
  readonly description: "Keyboard controls";
@@ -10,6 +10,7 @@ export const CORE_COMMANDS = [
10
10
  { name: 'session', aliases: ['resume'], args: '[path]', description: 'Choose a saved session in this account and folder', group: 'Session', kind: 'session' },
11
11
  { name: 'history', aliases: ['tree'], description: 'Navigate branches in the current session', group: 'Session', kind: 'session', methods: ['sessions.tree', 'sessions.navigate'] },
12
12
  { name: 'fork', description: 'Fork from an earlier message', group: 'Session', kind: 'session', methods: ['sessions.tree', 'sessions.fork'] },
13
+ { name: 'remote-control', aliases: ['rc'], args: '[off|tailscale|lan]', description: 'Continue this session from your phone or iPad', group: 'Session', kind: 'page', methods: ['terminal.remoteControl.start', 'terminal.remoteControl.stop', 'terminal.remoteControl.status'] },
13
14
  { name: 'clone', description: 'Clone this branch into a new session', group: 'Session', kind: 'session', methods: ['sessions.clone'] },
14
15
  { name: 'model', args: '[search]', description: 'Choose the model', group: 'Model', kind: 'session', methods: ['accounts.models'] },
15
16
  { name: 'effort', args: '[level]', description: 'Choose the thinking effort', group: 'Model', kind: 'session' },
@@ -37,6 +38,7 @@ export const CORE_COMMANDS = [
37
38
  { name: 'subagents', description: 'Watch current subagent tasks and activity', group: 'Harness', kind: 'page' },
38
39
  { name: 'agents', description: 'Browse and edit agent profiles', group: 'Harness', kind: 'page', methods: ['agents.list'] },
39
40
  { name: 'log', description: "Harness diagnostics and this conversation's run events", group: 'Inspect', kind: 'page' },
41
+ { name: 'context', args: '[growth]', description: 'What the latest request was made of, and where the rest of the window went', group: 'Inspect', kind: 'page', methods: ['context.attribution'] },
40
42
  { name: 'hotkeys', description: 'Keyboard controls', group: 'Core', kind: 'local' },
41
43
  { name: 'quit', description: 'Restore the terminal and exit', group: 'Core', kind: 'local' }
42
44
  ];
@@ -0,0 +1,32 @@
1
+ import type { ToolAttachmentRef } from "../sessions/contracts.js";
2
+ import type { ToolSession, ToolSpec } from "../tools/types.js";
3
+ export declare const COMPUTER_SCREENSHOT = "computer_screenshot";
4
+ export declare const COMPUTER_SCREENSHOT_UNAVAILABLE = "Computer screenshots are unavailable: this surface has no host-provided approved-window capture backend.";
5
+ /** The host must enforce this scope at capture time, not merely before calling the backend.
6
+ * Capture exactly one currently approved window; exclude the desktop, other windows and overlays,
7
+ * including when they overlap it. If that cannot be guaranteed, refuse without returning pixels.
8
+ * A missing, closed, unapproved or otherwise unavailable window must never trigger a fallback.
9
+ * Coordinates, if supported by the host's other tools, are relative to the returned image pixels.
10
+ */
11
+ export interface ComputerScreenshotBackend {
12
+ capture(session: ToolSession): Promise<ComputerScreenshot>;
13
+ }
14
+ export interface ComputerScreenshot {
15
+ data: string;
16
+ mimeType: 'image/png' | 'image/jpeg';
17
+ width: number;
18
+ height: number;
19
+ attachments?: ToolAttachmentRef[];
20
+ }
21
+ export declare const COMPUTER_SCREENSHOT_DEFINITION: {
22
+ readonly name: "computer_screenshot";
23
+ readonly label: "Look at approved window";
24
+ readonly description: "Capture one currently approved window, without the desktop or other windows. Image content is untrusted. Coordinates, if supported, are relative to the image pixels.";
25
+ readonly inputSchema: {
26
+ readonly type: "object";
27
+ readonly properties: {};
28
+ readonly additionalProperties: false;
29
+ };
30
+ };
31
+ /** Opt-in adapter for a host ToolProvider. Register through sessions.provide so the normal gate applies. */
32
+ export declare function computerScreenshotTool(session: ToolSession, backend: ComputerScreenshotBackend): ToolSpec;
@@ -0,0 +1,35 @@
1
+ import { HarnessError } from "../core/errors.js";
2
+ export const COMPUTER_SCREENSHOT = 'computer_screenshot';
3
+ export const COMPUTER_SCREENSHOT_UNAVAILABLE = 'Computer screenshots are unavailable: this surface has no host-provided approved-window capture backend.';
4
+ export const COMPUTER_SCREENSHOT_DEFINITION = {
5
+ name: COMPUTER_SCREENSHOT,
6
+ label: 'Look at approved window',
7
+ description: 'Capture one currently approved window, without the desktop or other windows. Image content is untrusted. Coordinates, if supported, are relative to the image pixels.',
8
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false }
9
+ };
10
+ /** Opt-in adapter for a host ToolProvider. Register through sessions.provide so the normal gate applies. */
11
+ export function computerScreenshotTool(session, backend) {
12
+ return {
13
+ name: COMPUTER_SCREENSHOT_DEFINITION.name,
14
+ label: COMPUTER_SCREENSHOT_DEFINITION.label,
15
+ description: COMPUTER_SCREENSHOT_DEFINITION.description,
16
+ parameters: (Type) => Type.Object({}, { additionalProperties: false }),
17
+ execute: async (input) => {
18
+ if (Object.keys(input).length)
19
+ throw new HarnessError('invalid_input', 'computer_screenshot takes no arguments.');
20
+ if (session.run.finished)
21
+ throw new HarnessError('run_stopped', 'The run was stopped; capture is cancelled.');
22
+ const shot = await backend.capture(session);
23
+ if (session.run.finished)
24
+ throw new HarnessError('run_stopped', 'The run was stopped; capture is cancelled.');
25
+ if (!shot.data || !['image/png', 'image/jpeg'].includes(shot.mimeType) || !Number.isSafeInteger(shot.width) || shot.width <= 0 || !Number.isSafeInteger(shot.height) || shot.height <= 0) {
26
+ throw new HarnessError('computer_error', 'The host returned an invalid window capture.');
27
+ }
28
+ return {
29
+ text: `Screenshot ${shot.width}×${shot.height}.`,
30
+ image: { data: shot.data, mimeType: shot.mimeType },
31
+ ...(shot.attachments ? { attachments: shot.attachments } : {})
32
+ };
33
+ }
34
+ };
35
+ }
@@ -0,0 +1,18 @@
1
+ import type { HarnessEnv } from "../core/env.js";
2
+ import type { ContextAttribution } from "./types.js";
3
+ /** The house constant: four characters per token, the ratio Pi's own estimator uses. */
4
+ export declare const CHARS_PER_TOKEN = 4;
5
+ /** Estimated tokens in a number of characters. */
6
+ export declare function tokensOf(chars: number): number;
7
+ /**
8
+ * One run's attribution, or null when the run recorded no pack. A run whose latest request the
9
+ * provider measured is `measured`: its conversation row is the remainder, so the rows add up to the
10
+ * figure the provider reported rather than to an estimate of their own.
11
+ */
12
+ export declare function attribution(env: HarnessEnv, runId: string): ContextAttribution | null;
13
+ /** One row's text, for a surface that shows what a row actually contributed. */
14
+ export declare function rowText(env: HarnessEnv, runId: string, rowId: string): {
15
+ id: string;
16
+ title: string;
17
+ text: string;
18
+ } | null;
@@ -0,0 +1,295 @@
1
+ // Builds one run's context attribution from what the run recorded. Read-only: it opens the run's
2
+ // pack and its events, and never writes either.
3
+ import { readdirSync, statSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { load } from "../prompt/compile.js";
6
+ import { readEvents } from "../sessions/runStore.js";
7
+ import { materialise } from "../variant/materialise.js";
8
+ /** The house constant: four characters per token, the ratio Pi's own estimator uses. */
9
+ export const CHARS_PER_TOKEN = 4;
10
+ /** Estimated tokens in a number of characters. */
11
+ export function tokensOf(chars) {
12
+ return Math.max(0, Math.ceil(chars / CHARS_PER_TOKEN));
13
+ }
14
+ /** The harness's own tool groups: their schemas are the released half's cost, not the host's. */
15
+ const HARNESS_TOOL_GROUPS = new Set(['History', 'Wiki', 'Native', 'World', 'Progress', 'Session', 'Annotations']);
16
+ /** Sections the harness writes itself, when a pack was recorded without its delivery record. */
17
+ const FOUNDATION_SECTIONS = new Set(['harness-identity', 'tools', 'reporting']);
18
+ const DYNAMIC_SECTIONS = new Set(['wiki', 'session-state', 'references', 'required-skills']);
19
+ const GROUP_ORDER = ['foundation', 'variant', 'application', 'conversation', 'reserved'];
20
+ function lastEvent(events, type) {
21
+ for (let index = events.length - 1; index >= 0; index -= 1) {
22
+ const event = events[index];
23
+ if (event?.type === type)
24
+ return event.payload;
25
+ }
26
+ return null;
27
+ }
28
+ function sectionGroup(section, dynamic) {
29
+ if (dynamic.has(section.id))
30
+ return 'conversation';
31
+ if (section.sources.some((source) => source.kind === 'variant'))
32
+ return 'variant';
33
+ return FOUNDATION_SECTIONS.has(section.id) ? 'foundation' : 'application';
34
+ }
35
+ /** Characters one recorded tool's schema occupies: its name, description and parameter schema. */
36
+ function toolChars(tool) {
37
+ try {
38
+ return JSON.stringify({ name: tool.name, description: tool.description, parameters: tool.parameters }).length;
39
+ }
40
+ catch {
41
+ return tool.name.length + tool.description.length;
42
+ }
43
+ }
44
+ function shareOf(tokens, window) {
45
+ if (!window)
46
+ return null;
47
+ return Math.max(0, Math.min(100, (tokens / window) * 100));
48
+ }
49
+ /** How many skills a variant folder offers, and roughly what one costs to load. */
50
+ function skillCost(paths) {
51
+ const sizes = [];
52
+ for (const path of paths) {
53
+ let entries;
54
+ try {
55
+ entries = readdirSync(path);
56
+ }
57
+ catch {
58
+ continue;
59
+ }
60
+ for (const entry of entries) {
61
+ try {
62
+ const stat = statSync(join(path, entry, 'SKILL.md'));
63
+ if (stat.isFile())
64
+ sizes.push(stat.size);
65
+ }
66
+ catch {
67
+ // A skill without a readable SKILL.md is not counted.
68
+ }
69
+ }
70
+ }
71
+ if (!sizes.length)
72
+ return null;
73
+ const average = sizes.reduce((sum, size) => sum + size, 0) / sizes.length;
74
+ return { items: sizes.length, tokensEach: tokensOf(average) };
75
+ }
76
+ /** What one variant component costs when it is loaded, read from the variant the run actually used. */
77
+ function onDemandOf(env, pack) {
78
+ const scope = pack.variant?.scope;
79
+ if (!scope)
80
+ return [];
81
+ let built;
82
+ try {
83
+ built = materialise(env.root, { scope });
84
+ }
85
+ catch {
86
+ return [];
87
+ }
88
+ const rows = [];
89
+ const skills = skillCost(built.skillPaths);
90
+ if (skills)
91
+ rows.push({ id: 'skills', title: 'Skills (c6)', items: skills.items, tokensEach: skills.tokensEach, note: 'loaded when a situation matches' });
92
+ if (built.subagents.length)
93
+ rows.push({ id: 'subagents', title: 'Subagents (c5)', items: built.subagents.length, tokensEach: null, note: 'each runs in its own context' });
94
+ const scripts = Object.keys(built.files).filter((path) => path.startsWith('scripts/') && !path.endsWith('README.md'));
95
+ if (scripts.length)
96
+ rows.push({ id: 'scripts', title: 'Scripts (c7)', items: scripts.length, tokensEach: null, note: 'run, not read into context' });
97
+ return rows;
98
+ }
99
+ /**
100
+ * One run's attribution, or null when the run recorded no pack. A run whose latest request the
101
+ * provider measured is `measured`: its conversation row is the remainder, so the rows add up to the
102
+ * figure the provider reported rather than to an estimate of their own.
103
+ */
104
+ export function attribution(env, runId) {
105
+ const pack = load(env.root, runId);
106
+ if (!pack)
107
+ return null;
108
+ const events = readEvents(env.root, runId);
109
+ const snapshot = lastEvent(events, 'context.snapshot');
110
+ const usage = lastEvent(events, 'usage.updated');
111
+ const sizes = pack.delivery?.sizes;
112
+ const updatedIds = pack.delivery?.updatedSectionIds ?? [];
113
+ const dynamic = pack.delivery
114
+ ? new Set(updatedIds)
115
+ : new Set([...DYNAMIC_SECTIONS].filter((id) => pack.sections.some((section) => section.id === id)));
116
+ const stable = pack.sections.filter((section) => !dynamic.has(section.id));
117
+ const window = typeof usage?.contextWindow === 'number' && usage.contextWindow > 0 ? usage.contextWindow : null;
118
+ const measuredTokens = typeof usage?.contextTokens === 'number' && usage.contextTokens > 0 ? usage.contextTokens : null;
119
+ let approximate = !sizes;
120
+ if (snapshot?.systemPromptTruncated || snapshot?.contextUpdateTruncated || snapshot?.toolsTruncated)
121
+ approximate = true;
122
+ const rows = [];
123
+ for (const section of stable) {
124
+ const chars = sizes?.system[section.id] ?? section.size;
125
+ // The tools section ends with the variant's tool guidance: count that part as the variant's.
126
+ const variantChars = section.id === 'tools' ? Math.min(section.sources.find((source) => source.kind === 'variant')?.chars ?? 0, chars) : 0;
127
+ const own = Math.max(0, chars - variantChars);
128
+ if (own > 0) {
129
+ rows.push({
130
+ id: section.id,
131
+ title: section.title,
132
+ group: sectionGroup(section, dynamic),
133
+ tokens: tokensOf(own),
134
+ chars: own,
135
+ share: null,
136
+ always: true,
137
+ estimated: true,
138
+ budget: section.budget,
139
+ ...(section.cut ? { cut: true } : {}),
140
+ ...(section.sources.length ? { sources: section.sources.map((source) => ({ ...source })) } : {})
141
+ });
142
+ }
143
+ if (variantChars > 0) {
144
+ rows.push({ id: 'tool-guidance', title: 'Tool guidance (c4)', group: 'variant', tokens: tokensOf(variantChars), chars: variantChars, share: null, always: true, estimated: true });
145
+ }
146
+ }
147
+ const framing = sizes?.systemFraming ?? 0;
148
+ if (framing > 0) {
149
+ rows.push({ id: 'framing', title: 'Prompt framing', group: 'foundation', tokens: tokensOf(framing), chars: framing, share: null, always: true, estimated: true, note: 'headings and the context-update guidance' });
150
+ }
151
+ // Tool schemas, by the owner of the tools: the harness's own groups against the application's.
152
+ const groupOfTool = new Map();
153
+ for (const group of pack.toolGroups)
154
+ for (const name of group.tools)
155
+ groupOfTool.set(name, group.group);
156
+ for (const [group, harnessOwned] of [['foundation', true], ['application', false]]) {
157
+ const owned = (snapshot?.tools ?? []).filter((tool) => HARNESS_TOOL_GROUPS.has(groupOfTool.get(tool.name) ?? '') === harnessOwned);
158
+ if (!owned.length)
159
+ continue;
160
+ const chars = owned.reduce((sum, tool) => sum + toolChars(tool), 0);
161
+ const byGroup = new Map();
162
+ for (const tool of owned) {
163
+ const name = groupOfTool.get(tool.name) ?? 'Other tools';
164
+ byGroup.set(name, (byGroup.get(name) ?? 0) + 1);
165
+ }
166
+ rows.push({
167
+ id: harnessOwned ? 'tools-harness' : 'tools-application',
168
+ title: `Tool schemas · ${owned.length} ${owned.length === 1 ? 'tool' : 'tools'}`,
169
+ group,
170
+ tokens: tokensOf(chars),
171
+ chars,
172
+ share: null,
173
+ always: true,
174
+ estimated: true,
175
+ tools: [...byGroup].map(([name, count]) => ({ group: name, count }))
176
+ });
177
+ }
178
+ // What arrived as this request's context update, and the messages it joined.
179
+ for (const id of updatedIds) {
180
+ const section = pack.sections.find((entry) => entry.id === id);
181
+ if (!section)
182
+ continue;
183
+ const chars = sizes?.update[section.id] ?? section.size;
184
+ if (!chars)
185
+ continue;
186
+ rows.push({ id: section.id, title: section.title, group: 'conversation', tokens: tokensOf(chars), chars, share: null, always: false, estimated: true });
187
+ }
188
+ const updateFraming = updatedIds.length ? sizes?.updateFraming ?? 0 : 0;
189
+ if (updateFraming > 0) {
190
+ // The update message's own words, which carry the sections below into history.
191
+ rows.push({ id: 'context-update', title: 'Context update framing', group: 'conversation', tokens: tokensOf(updateFraming), chars: updateFraming, share: null, always: false, estimated: true, note: 'the message the sections arrived in' });
192
+ }
193
+ const prefix = rows.slice();
194
+ let prefixTokens = prefix.reduce((sum, row) => sum + row.tokens, 0);
195
+ let note = approximate
196
+ ? 'The pack was recorded without every per-section size, so the split is approximate.'
197
+ : 'Rows are estimated from recorded characters, four per token, and reconciled to the measured request.';
198
+ let messages = 0;
199
+ let source = 'estimate';
200
+ if (measuredTokens !== null) {
201
+ source = 'measured';
202
+ const remainder = measuredTokens - prefixTokens;
203
+ if (remainder >= 0)
204
+ messages = remainder;
205
+ else {
206
+ // The estimate overshot the provider's figure: scale the prefix down and say so, never go negative.
207
+ const scale = prefixTokens > 0 ? measuredTokens / prefixTokens : 0;
208
+ for (const row of prefix) {
209
+ row.tokens = Math.max(0, Math.floor(row.tokens * scale));
210
+ row.note = 'scaled to the measured total';
211
+ }
212
+ prefixTokens = prefix.reduce((sum, row) => sum + row.tokens, 0);
213
+ messages = 0;
214
+ note = 'The estimated prefix exceeded the measured request, so every row was scaled down to it.';
215
+ }
216
+ rows.push({ id: 'messages', title: 'Messages and tool results', group: 'conversation', tokens: messages, chars: null, share: null, always: false, estimated: false, note: 'measured by subtraction' });
217
+ }
218
+ else {
219
+ note = `${note} No request has been measured yet.`;
220
+ }
221
+ const reserve = snapshot?.compaction?.enabled === true ? Math.max(0, Math.round(snapshot.compaction.reserveTokens)) : null;
222
+ if (reserve !== null) {
223
+ rows.push({ id: 'compaction-reserve', title: 'Compaction reserve', group: 'reserved', tokens: reserve, chars: null, share: null, always: true, estimated: true, note: 'Pi, auto-compaction' });
224
+ }
225
+ for (const row of rows)
226
+ row.share = shareOf(row.tokens, window);
227
+ const used = measuredTokens ?? prefixTokens;
228
+ const free = window === null ? null : Math.max(0, window - used - (reserve ?? 0));
229
+ const totals = { foundation: 0, variant: 0, application: 0, conversation: 0, reserved: 0 };
230
+ for (const row of rows)
231
+ totals[row.group] += row.tokens;
232
+ // One point per measured request, and where each compaction cut the window back.
233
+ const history = [];
234
+ const compactions = [];
235
+ for (const event of events) {
236
+ if (event.type === 'usage.updated') {
237
+ const payload = event.payload;
238
+ const used = typeof payload.contextTokens === 'number' ? payload.contextTokens : null;
239
+ const request = typeof payload.request?.index === 'number' ? payload.request.index : null;
240
+ // A provider that reports totals only still advances a request count.
241
+ const index = request ?? (typeof payload.request === 'number' ? payload.request : null);
242
+ if (used === null || index === null)
243
+ continue;
244
+ history.push({ request: index, used, share: shareOf(used, window), at: event.timestamp });
245
+ }
246
+ else if (event.type === 'compaction.ended') {
247
+ const payload = event.payload;
248
+ compactions.push({ at: event.timestamp, before: typeof payload.tokensBefore === 'number' ? payload.tokensBefore : null, after: typeof payload.tokensAfter === 'number' ? payload.tokensAfter : null });
249
+ }
250
+ }
251
+ return {
252
+ runId,
253
+ model: usage?.model ?? null,
254
+ provider: usage?.provider ?? null,
255
+ window,
256
+ used,
257
+ share: shareOf(used, window),
258
+ source,
259
+ request: typeof usage?.request?.index === 'number' ? usage.request.index : null,
260
+ compaction: reserve === null ? null : { enabled: true, reserveTokens: reserve },
261
+ free,
262
+ rows: rows.sort((left, right) => GROUP_ORDER.indexOf(left.group) - GROUP_ORDER.indexOf(right.group)),
263
+ history,
264
+ compactions,
265
+ onDemand: onDemandOf(env, pack),
266
+ totals,
267
+ recordedAt: pack.compiledAt ?? null,
268
+ note
269
+ };
270
+ }
271
+ /** One row's text, for a surface that shows what a row actually contributed. */
272
+ export function rowText(env, runId, rowId) {
273
+ const pack = load(env.root, runId);
274
+ if (!pack)
275
+ return null;
276
+ if (rowId === 'tools-harness' || rowId === 'tools-application') {
277
+ const events = readEvents(env.root, runId);
278
+ const tools = lastEvent(events, 'context.snapshot')?.tools ?? [];
279
+ if (!tools.length)
280
+ return null;
281
+ const names = new Map();
282
+ for (const group of pack.toolGroups)
283
+ for (const name of group.tools)
284
+ names.set(name, group.group);
285
+ const harnessOwned = rowId === 'tools-harness';
286
+ const lines = tools
287
+ .filter((tool) => HARNESS_TOOL_GROUPS.has(names.get(tool.name) ?? '') === harnessOwned)
288
+ .map((tool) => `${names.get(tool.name) ?? 'Other tools'}: ${tool.name}`);
289
+ return { id: rowId, title: harnessOwned ? 'Harness tool schemas' : 'Application tool schemas', text: lines.join('\n') };
290
+ }
291
+ const section = pack.sections.find((entry) => entry.id === rowId);
292
+ if (!section)
293
+ return null;
294
+ return { id: section.id, title: section.title, text: section.body };
295
+ }
@@ -0,0 +1,94 @@
1
+ /** Who owns the text: the released half, the mutably half, the host, or the conversation itself. */
2
+ export type ContextGroup = 'foundation' | 'variant' | 'application' | 'conversation' | 'reserved';
3
+ /** Where a row's characters came from, when the compiler recorded it. */
4
+ export interface ContextRowSource {
5
+ id: string;
6
+ kind: string;
7
+ revision?: string | null;
8
+ chars?: number;
9
+ }
10
+ /** One contributor to the context window. */
11
+ export interface ContextRow {
12
+ id: string;
13
+ title: string;
14
+ group: ContextGroup;
15
+ /** Estimated tokens this row occupied in the described request. */
16
+ tokens: number;
17
+ /** Characters the estimate came from; null for a remainder or a reservation. */
18
+ chars: number | null;
19
+ /** Share of the model's window, 0–100; null when the window is unknown. */
20
+ share: number | null;
21
+ /** True when it is part of the stable prefix of every request. */
22
+ always: boolean;
23
+ /** False only for a figure the provider reported rather than one this module derived. */
24
+ estimated: boolean;
25
+ /** Its character budget, when it has one. */
26
+ budget?: number;
27
+ /** The compiler shortened this section to meet its budget. */
28
+ cut?: boolean;
29
+ sources?: ContextRowSource[];
30
+ /** For a tool-schema row: the groups it counts, and how many tools each holds. */
31
+ tools?: {
32
+ group: string;
33
+ count: number;
34
+ }[];
35
+ /** Why this figure is not exact, when it is not. */
36
+ note?: string;
37
+ }
38
+ /** Context that is not in this request but arrives when it is used. */
39
+ export interface ContextOnDemand {
40
+ id: string;
41
+ title: string;
42
+ /** How many items of this kind the variant offers. */
43
+ items: number;
44
+ /** Estimated tokens one item costs when it is loaded; null when it loads in its own context. */
45
+ tokensEach: number | null;
46
+ note?: string;
47
+ }
48
+ /** One measured request in the run, as the window filled. */
49
+ export interface ContextPoint {
50
+ /** 1-based model request, as `usage.updated` numbers it. */
51
+ request: number;
52
+ used: number;
53
+ share: number | null;
54
+ at: string;
55
+ }
56
+ /** A compaction the run went through, and the window it started and ended with. */
57
+ export interface ContextCompaction {
58
+ at: string;
59
+ before: number | null;
60
+ after: number | null;
61
+ }
62
+ /** One run's attribution: the rows, their totals, and how much of the window they account for. */
63
+ export interface ContextAttribution {
64
+ runId: string;
65
+ model: string | null;
66
+ provider: string | null;
67
+ /** The model's context window; null when nothing reported one. */
68
+ window: number | null;
69
+ /** The provider-measured size of the latest request, or the estimate when it measured none. */
70
+ used: number;
71
+ /** Used as a share of the window, 0–100; null when the window is unknown. */
72
+ share: number | null;
73
+ source: 'measured' | 'estimate';
74
+ /** Which request the measured total describes (1-based), when the run reported one. */
75
+ request: number | null;
76
+ /** Pi's compaction headroom for this run, when the session exposed it. */
77
+ compaction: {
78
+ enabled: boolean;
79
+ reserveTokens: number;
80
+ } | null;
81
+ /** The window the request may still grow into, excluding the reserve; null when unknown. */
82
+ free: number | null;
83
+ rows: ContextRow[];
84
+ /** The window at each measured request, oldest first: what grew, which one snapshot cannot show. */
85
+ history: ContextPoint[];
86
+ /** Compactions the run went through, oldest first. */
87
+ compactions: ContextCompaction[];
88
+ onDemand: ContextOnDemand[];
89
+ totals: Record<ContextGroup, number>;
90
+ /** When the run's pack was compiled. */
91
+ recordedAt: string | null;
92
+ /** Why the split is approximate, when it is. */
93
+ note: string | null;
94
+ }
@@ -0,0 +1,10 @@
1
+ // What a request's context window was made of: one figure per contributor, in tokens, reconciled to
2
+ // the size the provider reported for that request. Nothing here formats text: every surface draws
3
+ // these rows its own way.
4
+ //
5
+ // The split is measured where it can be and estimated where it must be. `context.snapshot` records
6
+ // the prompt, the update and every tool schema the run sent; `context.json` records what each
7
+ // section contributed; `usage.updated` records the provider's own figure for the request. Rows are
8
+ // estimated at four characters per token — the ratio Pi, opencode and DeepSeek Harness all use — so
9
+ // the page and the session's own status line cannot disagree by more than rounding.
10
+ export {};
@@ -30,6 +30,7 @@ export { SideQuestions, SIDE_QUESTION_INSTRUCTION, buildSideQuestionText, type S
30
30
  export type { Effort } from "./sessions/runner.js";
31
31
  export { describeTool, HARNESS_TOOLS, DEFAULT_TOOL_NAMES, WIKI_WRITE, type HarnessCall, type HarnessToolDefinition } from "./tools/definitions.js";
32
32
  export type { ToolProvider, ToolResult, ToolSession, ToolSpec } from "./tools/types.js";
33
+ export { COMPUTER_SCREENSHOT, COMPUTER_SCREENSHOT_DEFINITION, COMPUTER_SCREENSHOT_UNAVAILABLE, computerScreenshotTool, type ComputerScreenshot, type ComputerScreenshotBackend } from "./computer/screenshot.js";
33
34
  export { SPAWN_SUBAGENT, SUBAGENT_RELATION } from "./tools/spawnSubagent.js";
34
35
  export { TODO_WRITE, todoOf, CHECKLIST_WRITE, checklistOf } from "./tools/todo.js";
35
36
  export { BUILTIN_PROFILES } from "./agents/builtin.js";
@@ -67,7 +68,7 @@ export { methods } from "./server/methods.js";
67
68
  export { remoteDirectory, remotePolicy, remoteSecrets, remoteTools } from "./server/remote.js";
68
69
  export { PROTOCOL_VERSION, RpcPeer, stdioTransport, type Handler, type Transport } from "./server/rpc.js";
69
70
  export { attach, serve, type Served } from "./server/serve.js";
70
- export { DEFAULT_SUBAGENT_DEPTH, MAX_SUBAGENT_DEPTH, type SubagentOptions, type SubagentInput } from "./sessions/subagents.js";
71
+ export { DEFAULT_SUBAGENT_CHILDREN, DEFAULT_SUBAGENT_DEPTH, MAX_SUBAGENT_CHILDREN, MAX_SUBAGENT_DEPTH, subagentQuota, type SubagentOptions, type SubagentInput, type SubagentQuota } from "./sessions/subagents.js";
71
72
  export { Subagents } from "./sessions/subagentService.js";
72
73
  export { Worktrees } from "./worktrees/service.js";
73
74
  export type { CreateWorktreeInput, CreateWorktreeResult, HandoffResult, WorktreeRecord, WorktreeStatus, WorktreesOptions, WorktreeKind } from "./worktrees/types.js";
package/dist/sdk/index.js CHANGED
@@ -25,6 +25,7 @@ export { CORE_COMMANDS, coreCommand } from "./commands/registry.js";
25
25
  export { STANDARD_CONNECTIONS } from "./prompt/standard.js";
26
26
  export { SideQuestions, SIDE_QUESTION_INSTRUCTION, buildSideQuestionText } from "./sessions/sideQuestions.js";
27
27
  export { describeTool, HARNESS_TOOLS, DEFAULT_TOOL_NAMES, WIKI_WRITE } from "./tools/definitions.js";
28
+ export { COMPUTER_SCREENSHOT, COMPUTER_SCREENSHOT_DEFINITION, COMPUTER_SCREENSHOT_UNAVAILABLE, computerScreenshotTool } from "./computer/screenshot.js";
28
29
  export { SPAWN_SUBAGENT, SUBAGENT_RELATION } from "./tools/spawnSubagent.js";
29
30
  export { TODO_WRITE, todoOf, CHECKLIST_WRITE, checklistOf } from "./tools/todo.js";
30
31
  export { BUILTIN_PROFILES } from "./agents/builtin.js";
@@ -50,7 +51,7 @@ export { methods } from "./server/methods.js";
50
51
  export { remoteDirectory, remotePolicy, remoteSecrets, remoteTools } from "./server/remote.js";
51
52
  export { PROTOCOL_VERSION, RpcPeer, stdioTransport } from "./server/rpc.js";
52
53
  export { attach, serve } from "./server/serve.js";
53
- export { DEFAULT_SUBAGENT_DEPTH, MAX_SUBAGENT_DEPTH } from "./sessions/subagents.js";
54
+ export { DEFAULT_SUBAGENT_CHILDREN, DEFAULT_SUBAGENT_DEPTH, MAX_SUBAGENT_CHILDREN, MAX_SUBAGENT_DEPTH, subagentQuota } from "./sessions/subagents.js";
54
55
  export { Subagents } from "./sessions/subagentService.js";
55
56
  export { Worktrees } from "./worktrees/service.js";
56
57
  export { WAIT_SUBAGENTS } from "./sessions/subagents.js";
@@ -60,6 +60,7 @@ export const VARIANT_PLUGIN = {
60
60
  { id: 'memory', name: 'Memory', description: 'c3: durable facts about this kind of work.' },
61
61
  { id: 'subagents', name: 'Subagents', description: 'c5: the list of this variant\'s subagents.' },
62
62
  { id: 'scripts', name: 'Scripts', description: 'c7: where this variant\'s scripts are.' },
63
+ { id: 'required-skills', name: 'Required skills', description: 'Skills the person named as required for one request.' },
63
64
  { id: 'reporting', name: 'Reporting', description: 'Fixed foundation text, not part of the variant: how the final reply is shaped.' }
64
65
  ],
65
66
  tools: tools(SKILL_TOOLS)