@astrosheep/keiyaku 4.0.3 → 4.0.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 (141) hide show
  1. package/build/integrations/marketplace/plugins/keiyaku/.claude-plugin/plugin.json +1 -1
  2. package/build/integrations/marketplace/plugins/keiyaku/.codex-plugin/plugin.json +1 -1
  3. package/build/integrations/marketplace/plugins/keiyaku/package.json +1 -1
  4. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +1 -1
  5. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +1 -1
  6. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +15 -1
  7. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-task/SKILL.md +3 -1
  8. package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +20 -13
  9. package/build/src/akuma/akuma.d.ts +34 -11
  10. package/build/src/akuma/akuma.js +121 -61
  11. package/build/src/akuma/allowed.d.ts +7 -0
  12. package/build/src/akuma/allowed.js +40 -0
  13. package/build/src/akuma/archetype.d.ts +7 -4
  14. package/build/src/akuma/archetype.js +15 -16
  15. package/build/src/akuma/body.d.ts +3 -1
  16. package/build/src/akuma/body.js +9 -6
  17. package/build/src/akuma/heart/facts.d.ts +49 -7
  18. package/build/src/akuma/heart/index.d.ts +4 -8
  19. package/build/src/akuma/heart/index.js +76 -15
  20. package/build/src/akuma/heart/rows.d.ts +7 -5
  21. package/build/src/akuma/heart/rows.js +98 -11
  22. package/build/src/akuma/heart/schema.d.ts +1 -1
  23. package/build/src/akuma/heart/schema.js +15 -10
  24. package/build/src/akuma/heart/soul.js +3 -1
  25. package/build/src/akuma/heart/storage.d.ts +1 -0
  26. package/build/src/akuma/heart/storage.js +15 -0
  27. package/build/src/akuma/heart/timeline.d.ts +1 -5
  28. package/build/src/akuma/heart/timeline.js +8 -17
  29. package/build/src/akuma/index.d.ts +4 -1
  30. package/build/src/akuma/index.js +1 -0
  31. package/build/src/akuma/projection.d.ts +17 -3
  32. package/build/src/akuma/projection.js +40 -3
  33. package/build/src/akuma/provider.d.ts +1 -1
  34. package/build/src/akuma/provider.js +2 -1
  35. package/build/src/akuma/providers/acp/core.d.ts +3 -1
  36. package/build/src/akuma/providers/acp/core.js +4 -4
  37. package/build/src/akuma/providers/acp/events.d.ts +11 -3
  38. package/build/src/akuma/providers/acp/events.js +78 -4
  39. package/build/src/akuma/providers/claude/events.js +62 -3
  40. package/build/src/akuma/providers/codex-app-server/events.js +1 -13
  41. package/build/src/akuma/providers/grok-build/index.d.ts +2 -1
  42. package/build/src/akuma/providers/grok-build/index.js +72 -2
  43. package/build/src/akuma/providers/opencode-sdk/events.js +71 -6
  44. package/build/src/akuma/providers/pi/events.js +17 -3
  45. package/build/src/akuma/providers/unified-patch.d.ts +4 -0
  46. package/build/src/akuma/providers/unified-patch.js +14 -0
  47. package/build/src/akuma/publication.d.ts +1 -1
  48. package/build/src/akuma/publication.js +1 -1
  49. package/build/src/akuma/requests.d.ts +70 -4
  50. package/build/src/akuma/requests.js +288 -77
  51. package/build/src/akuma-body.d.ts +1 -0
  52. package/build/src/akuma-body.js +31 -0
  53. package/build/src/cli/accepted.d.ts +4 -1
  54. package/build/src/cli/accepted.js +26 -4
  55. package/build/src/cli/commands/akuma-invoke.d.ts +8 -7
  56. package/build/src/cli/commands/akuma-invoke.js +18 -10
  57. package/build/src/cli/commands/akuma.d.ts +11 -2
  58. package/build/src/cli/commands/akuma.js +65 -26
  59. package/build/src/cli/commands/amend.d.ts +1 -1
  60. package/build/src/cli/commands/amend.js +1 -1
  61. package/build/src/cli/commands/contract.d.ts +3 -3
  62. package/build/src/cli/commands/contract.js +1 -1
  63. package/build/src/cli/commands/install.js +2 -2
  64. package/build/src/cli/commands/task-query.d.ts +1 -1
  65. package/build/src/cli/commands/task-query.js +11 -5
  66. package/build/src/cli/coordinates.d.ts +1 -0
  67. package/build/src/cli/coordinates.js +5 -1
  68. package/build/src/cli/invoke.d.ts +1 -0
  69. package/build/src/cli/invoke.js +90 -73
  70. package/build/src/cli/main.d.ts +2 -0
  71. package/build/src/cli/main.js +29 -4
  72. package/build/src/cli/parse.d.ts +1 -0
  73. package/build/src/cli/parse.js +4 -0
  74. package/build/src/cli/render/akuma.d.ts +2 -2
  75. package/build/src/cli/render/akuma.js +84 -13
  76. package/build/src/cli/render/audit.js +16 -2
  77. package/build/src/cli/render/contract.d.ts +2 -0
  78. package/build/src/cli/render/contract.js +203 -36
  79. package/build/src/cli/render/kanshi.js +64 -29
  80. package/build/src/cli/render/receipt.d.ts +2 -1
  81. package/build/src/cli/render/receipt.js +9 -1
  82. package/build/src/cli/render/text.js +3 -1
  83. package/build/src/cli/result.d.ts +38 -3
  84. package/build/src/cli/selectors.d.ts +1 -0
  85. package/build/src/cli/selectors.js +9 -6
  86. package/build/src/contract-worktree.js +3 -0
  87. package/build/src/dispatch/index.js +50 -39
  88. package/build/src/git/hooks.d.ts +0 -4
  89. package/build/src/git/hooks.js +0 -36
  90. package/build/src/git/integration.d.ts +2 -6
  91. package/build/src/git/integration.js +8 -8
  92. package/build/src/git/reconcile.d.ts +6 -1
  93. package/build/src/git/reconcile.js +55 -9
  94. package/build/src/git/tender.d.ts +19 -4
  95. package/build/src/git/tender.js +44 -13
  96. package/build/src/git/terminal-seal.d.ts +14 -0
  97. package/build/src/git/terminal-seal.js +15 -2
  98. package/build/src/git/workspace.d.ts +1 -0
  99. package/build/src/git/workspace.js +12 -4
  100. package/build/src/index.d.ts +1 -1
  101. package/build/src/kanshi/index.d.ts +1 -1
  102. package/build/src/kanshi/index.js +1 -1
  103. package/build/src/kanshi/read.d.ts +9 -1
  104. package/build/src/kanshi/read.js +91 -37
  105. package/build/src/kanshi/report.d.ts +1 -0
  106. package/build/src/library/address.d.ts +5 -14
  107. package/build/src/library/address.js +20 -22
  108. package/build/src/library/akuma-creation.d.ts +4 -2
  109. package/build/src/library/akuma-creation.js +43 -26
  110. package/build/src/library/bind.js +3 -3
  111. package/build/src/library/catalog.d.ts +1 -2
  112. package/build/src/library/catalog.js +11 -6
  113. package/build/src/library/configuration.d.ts +4 -0
  114. package/build/src/library/configuration.js +19 -0
  115. package/build/src/library/contract.d.ts +15 -1
  116. package/build/src/library/contract.js +51 -4
  117. package/build/src/library/fleet.d.ts +47 -9
  118. package/build/src/library/fleet.js +184 -44
  119. package/build/src/library/input.js +1 -0
  120. package/build/src/library/keiyaku.d.ts +4 -4
  121. package/build/src/library/keiyaku.js +1 -2
  122. package/build/src/protocol/bind.d.ts +1 -2
  123. package/build/src/protocol/bind.js +2 -16
  124. package/build/src/protocol/operations.d.ts +4 -1
  125. package/build/src/protocol/operations.js +33 -27
  126. package/build/src/protocol/read/status.d.ts +3 -1
  127. package/build/src/protocol/read/status.js +24 -14
  128. package/build/src/runtime/proc/run.d.ts +1 -0
  129. package/build/src/runtime/proc/run.js +9 -2
  130. package/build/src/settlement/settle.d.ts +1 -0
  131. package/build/src/settlement/settle.js +4 -1
  132. package/build/src/task/board.d.ts +18 -3
  133. package/build/src/task/board.js +73 -26
  134. package/build/src/task/index.d.ts +3 -3
  135. package/build/src/task/index.js +7 -5
  136. package/build/src/task/operations.d.ts +2 -19
  137. package/build/src/task/operations.js +29 -7
  138. package/build/src/task/query.d.ts +9 -2
  139. package/build/src/task/query.js +32 -64
  140. package/build/src/verification/execution.js +2 -1
  141. package/package.json +3 -1
@@ -1,4 +1,4 @@
1
- const HEART_SCHEMA_VERSION = 14;
1
+ const HEART_SCHEMA_VERSION = 16;
2
2
  const LEASH_SCHEMA_VERSION = 4;
3
3
  function assertSchemaVersion(database, table, expected) {
4
4
  const row = database.prepare(`SELECT version FROM ${table} WHERE singleton = 1`).get();
@@ -108,21 +108,26 @@ export const HEART_SCHEMA = `
108
108
  CREATE TABLE IF NOT EXISTS requests (
109
109
  sequence INTEGER PRIMARY KEY AUTOINCREMENT,
110
110
  id TEXT NOT NULL UNIQUE,
111
- archetype TEXT NOT NULL,
112
- body TEXT NOT NULL,
113
- cwd TEXT,
114
- world TEXT NOT NULL,
115
- recipe_json TEXT NOT NULL CHECK (json_valid(recipe_json)),
111
+ requester TEXT NOT NULL,
112
+ action TEXT NOT NULL CHECK (action IN ('akuma.call', 'akuma.wait', 'akuma.tell', 'akuma.kill')),
113
+ payload_json TEXT NOT NULL CHECK (json_valid(payload_json)),
116
114
  admitted_at TEXT NOT NULL,
117
115
  state TEXT NOT NULL CHECK (state IN ('admitted', 'reserved', 'served', 'refused', 'voided')),
118
116
  child TEXT,
117
+ service_json TEXT CHECK (service_json IS NULL OR json_valid(service_json)),
119
118
  diagnostic TEXT,
120
119
  evidence TEXT,
121
120
  CHECK (
122
- (state = 'admitted' AND child IS NULL AND diagnostic IS NULL AND evidence IS NULL)
123
- OR (state IN ('reserved', 'served') AND child IS NOT NULL AND diagnostic IS NULL AND evidence IS NULL)
124
- OR (state = 'refused' AND child IS NULL AND diagnostic IS NOT NULL AND evidence IS NULL)
125
- OR (state = 'voided' AND child IS NULL AND diagnostic IS NULL AND evidence IS NOT NULL)
121
+ (state = 'admitted' AND child IS NULL AND service_json IS NULL AND diagnostic IS NULL AND evidence IS NULL)
122
+ OR (state = 'reserved' AND action = 'akuma.call'
123
+ AND child IS NOT NULL AND service_json IS NULL
124
+ AND diagnostic IS NULL AND evidence IS NULL)
125
+ OR (state = 'served'
126
+ AND ((action = 'akuma.call' AND child IS NOT NULL AND service_json IS NULL)
127
+ OR (action != 'akuma.call' AND child IS NULL AND service_json IS NOT NULL))
128
+ AND diagnostic IS NULL AND evidence IS NULL)
129
+ OR (state = 'refused' AND child IS NULL AND service_json IS NULL AND diagnostic IS NOT NULL AND evidence IS NULL)
130
+ OR (state = 'voided' AND child IS NULL AND service_json IS NULL AND diagnostic IS NULL AND evidence IS NOT NULL)
126
131
  )
127
132
  ) STRICT;
128
133
  CREATE TABLE IF NOT EXISTS control (
@@ -1,5 +1,6 @@
1
1
  import { parseAkuId } from "../identity.js";
2
2
  import { decodeProviderOptions, decodeProviderRecipe, decodeReadonlyRestraint, } from "../provider-recipe.js";
3
+ import { effectiveAllowedActions } from "../allowed.js";
3
4
  function record(value) {
4
5
  return value !== null && typeof value === "object" && !Array.isArray(value)
5
6
  ? value
@@ -78,7 +79,7 @@ function validateSoul(value) {
78
79
  const soul = record(value);
79
80
  if (soul === null)
80
81
  throw new Error("Akuma soul must be an object");
81
- if (unknownKey(soul, ["archetype", "confinement", "createdAt", "cwd", "description", "id", "options", "origin", "provider", "readonly"]) !== undefined) {
82
+ if (unknownKey(soul, ["allowed", "archetype", "confinement", "createdAt", "cwd", "description", "id", "options", "origin", "provider", "readonly"]) !== undefined) {
82
83
  throw new Error("Akuma soul has an unknown field");
83
84
  }
84
85
  const identity = soulAkuIdentity(soul.id, "id");
@@ -103,6 +104,7 @@ function validateSoul(value) {
103
104
  cwd: nonblank(soul.cwd, "cwd"),
104
105
  origin: decodeOrigin(soul.origin),
105
106
  confinement: decodeConfinement(soul.confinement),
107
+ allowed: effectiveAllowedActions(soul.allowed),
106
108
  createdAt: nonblank(soul.createdAt, "createdAt"),
107
109
  };
108
110
  }
@@ -9,6 +9,7 @@ export declare class HeartAbsentError extends Error {
9
9
  export declare function isHeartAbsent(error: unknown): error is HeartAbsentError;
10
10
  export declare function withHeart<T>(paths: AkumaPaths, body: (database: DatabaseSync) => T): Promise<T>;
11
11
  export declare function transaction<T>(database: DatabaseSync, body: () => T): T;
12
+ export declare function readTransaction<T>(database: DatabaseSync, body: () => T): T;
12
13
  export declare function initializeHeart(paths: AkumaPaths): Promise<void>;
13
14
  export declare class HeldAkumaLeash {
14
15
  private readonly database;
@@ -84,6 +84,21 @@ export function transaction(database, body) {
84
84
  throw error;
85
85
  }
86
86
  }
87
+ export function readTransaction(database, body) {
88
+ database.exec("BEGIN DEFERRED");
89
+ try {
90
+ const result = body();
91
+ database.exec("COMMIT");
92
+ return result;
93
+ }
94
+ catch (error) {
95
+ try {
96
+ database.exec("ROLLBACK");
97
+ }
98
+ catch { /* preserve the original failure */ }
99
+ throw error;
100
+ }
101
+ }
87
102
  export async function initializeHeart(paths) {
88
103
  const heart = new DatabaseSync(paths.heart);
89
104
  try {
@@ -8,8 +8,4 @@ export type ActivityFactSlice = Readonly<{
8
8
  highest: number | null;
9
9
  }>;
10
10
  export declare function pruneActivityFacts(database: DatabaseSync, limit: number): void;
11
- export declare function activityFactSlice(database: DatabaseSync, input: Readonly<{
12
- before?: number;
13
- since?: number;
14
- limit: number;
15
- }>): ActivityFactSlice;
11
+ export declare function activityFactSlice(database: DatabaseSync): ActivityFactSlice;
@@ -64,23 +64,14 @@ function decodeTimelineRow(database, row) {
64
64
  }
65
65
  return decodeTellAtSequence(database, row.sequence);
66
66
  }
67
- export function activityFactSlice(database, input) {
67
+ export function activityFactSlice(database) {
68
68
  const bounds = database.prepare("SELECT MIN(sequence) AS lowest, MAX(sequence) AS highest FROM timeline")
69
69
  .get();
70
- let rows;
71
- if (input.before !== undefined) {
72
- rows = database.prepare(`SELECT * FROM (
73
- SELECT sequence, kind FROM timeline WHERE sequence < ? ORDER BY sequence DESC LIMIT ?
74
- ) ORDER BY sequence`).all(input.before, input.limit);
75
- }
76
- else if (input.since !== undefined) {
77
- rows = database.prepare(`SELECT sequence, kind FROM timeline WHERE sequence > ? ORDER BY sequence LIMIT ?`)
78
- .all(input.since, input.limit);
79
- }
80
- else {
81
- rows = database.prepare(`SELECT * FROM (
82
- SELECT sequence, kind FROM timeline ORDER BY sequence DESC LIMIT ?
83
- ) ORDER BY sequence`).all(input.limit);
84
- }
85
- return { rows: rows.map((row) => decodeTimelineRow(database, row)), lowestRetained: bounds.lowest, highest: bounds.highest };
70
+ const rows = database.prepare("SELECT sequence, kind FROM timeline ORDER BY sequence")
71
+ .all();
72
+ return {
73
+ rows: rows.map((row) => decodeTimelineRow(database, row)),
74
+ lowestRetained: bounds.lowest,
75
+ highest: bounds.highest,
76
+ };
86
77
  }
@@ -1,13 +1,16 @@
1
1
  import { type AkuId } from "./identity.js";
2
2
  import type { WorldRoot } from "../world.js";
3
3
  export { Akuma, AkumaHandle, AkumaNotBornError } from "./akuma.js";
4
+ export type { AkumaCallInput } from "./akuma.js";
5
+ export { ALLOWED_ACTIONS } from "./allowed.js";
6
+ export type { AllowedAction, AllowedActions } from "./allowed.js";
4
7
  export type { WorldRoot } from "../world.js";
5
8
  export declare function probeBornAkuma(worldPath: WorldRoot, id: AkuId): Promise<boolean>;
6
9
  export { AkumaArchetypeError, listArchetypeDefinitions, listArchetypes } from "./archetype.js";
7
10
  export type { ArchetypeCatalogRow } from "./archetype.js";
8
11
  export { AkumaBodyRequestError } from "./requests.js";
9
12
  export type { AkumaList, AkumaListInput, AkumaListRow, AkumaStatus, ActivityHistory, ActivityRow, ActivitySnapshot, ActivitySnapshotEntry, ForkReceipt, InterruptReceipt, TellResult, UnbornAkumaListRow, } from "./akuma.js";
10
- export type { ActiveToolRow, ClosedTurn, ClosedTurnRow, CompletedToolRow, HistoryCursor, HistoryPage, IdleSnapshotRow, OpenSnapshotRow, OpenTurn, OpenTurnRow, OutcomeRow, RetainedWindow, Snapshot, SnapshotRow, TellRow, TurnLedger, TurnOutcome, TurnStartRow, UnsettledToolRow, } from "./akuma.js";
13
+ export type { ActiveToolRow, ClosedTurn, ClosedTurnRow, CompletedToolRow, HistoryCursor, HistoryPage, IdleSnapshotRow, OpenSnapshotRow, OpenTurn, OpenTurnRow, OutcomeRow, ReportedFileChange, RetainedWindow, Snapshot, SnapshotRow, TellRow, TurnLedger, TurnOutcome, TurnStartRow, UnsettledToolRow, } from "./akuma.js";
11
14
  export type { AgentEvent, ToolCall, ToolEvent, ToolResult } from "./provider.js";
12
15
  export type { AkuId } from "./identity.js";
13
16
  export type { Confinement, KillEvidence, ResumeCoordinate, TurnFact, } from "./heart/index.js";
@@ -1,6 +1,7 @@
1
1
  import { readSoul } from "./heart/index.js";
2
2
  import { pathsForAkuId } from "./identity.js";
3
3
  export { Akuma, AkumaHandle, AkumaNotBornError } from "./akuma.js";
4
+ export { ALLOWED_ACTIONS } from "./allowed.js";
4
5
  export async function probeBornAkuma(worldPath, id) {
5
6
  const soul = await readSoul(pathsForAkuId(worldPath, id));
6
7
  return soul !== null;
@@ -114,21 +114,35 @@ export type ActivitySnapshotEntry<Row extends SnapshotRow = SnapshotRow> = Reado
114
114
  kind: "gap";
115
115
  count: number;
116
116
  }>;
117
+ export type ReportedFileChange = Readonly<{
118
+ sequence: number;
119
+ at: string;
120
+ op: "add" | "update" | "delete" | "unspecified";
121
+ path: string;
122
+ diffstat?: Readonly<{
123
+ added: number;
124
+ removed: number;
125
+ }>;
126
+ }>;
127
+ type ReportedChangeSummary = Readonly<{
128
+ reportedChanges: readonly ReportedFileChange[];
129
+ reportedChangesOmitted: number;
130
+ }>;
117
131
  export type Snapshot = Readonly<{
118
132
  kind: "unborn";
119
133
  entries: readonly [];
120
134
  omitted: 0;
121
- }> | Readonly<{
135
+ } & ReportedChangeSummary> | Readonly<{
122
136
  kind: "open";
123
137
  turn: TurnStartRow;
124
138
  entries: readonly ActivitySnapshotEntry<OpenSnapshotRow>[];
125
139
  omitted: number;
126
- }> | Readonly<{
140
+ } & ReportedChangeSummary> | Readonly<{
127
141
  kind: "idle";
128
142
  outcome?: OutcomeRow;
129
143
  entries: readonly ActivitySnapshotEntry<IdleSnapshotRow>[];
130
144
  omitted: number;
131
- }>;
145
+ } & ReportedChangeSummary>;
132
146
  export type ActivitySnapshot = Snapshot;
133
147
  export type ActivityHistory = Readonly<{
134
148
  rows: readonly ActivityRow[];
@@ -1,6 +1,8 @@
1
1
  import { decodeAgentEvent } from "./provider.js";
2
2
  const DEFAULT_TAIL = 3;
3
3
  const DEFAULT_VOICE = 3;
4
+ const REPORTED_CHANGE_LIMIT = 5;
5
+ const EMPTY_REPORTED = { reportedChanges: [], reportedChangesOmitted: 0 };
4
6
  function duration(startedAt, completedAt) {
5
7
  const start = Date.parse(startedAt);
6
8
  const end = Date.parse(completedAt);
@@ -204,11 +206,45 @@ export function ordinarySelectedCount(snapshot) {
204
206
  return count + 1;
205
207
  }, 0);
206
208
  }
209
+ function isSuccessfulFileChange(row) {
210
+ return row.kind === "tool"
211
+ && typeof row.state === "object"
212
+ && row.state.status === "ok"
213
+ && row.call.kind === "fileChange";
214
+ }
215
+ function flattenReportedChanges(rows) {
216
+ return rows.flatMap((row) => {
217
+ if (!isSuccessfulFileChange(row))
218
+ return [];
219
+ return row.call.changes.map((change) => ({
220
+ sequence: row.sequence,
221
+ at: row.at,
222
+ op: change.op,
223
+ path: change.path,
224
+ ...(change.diffstat === undefined ? {} : { diffstat: change.diffstat }),
225
+ }));
226
+ });
227
+ }
228
+ function summarizeReportedChanges(rows) {
229
+ const eligible = flattenReportedChanges(rows);
230
+ return {
231
+ reportedChanges: eligible.slice(-REPORTED_CHANGE_LIMIT),
232
+ reportedChangesOmitted: Math.max(0, eligible.length - REPORTED_CHANGE_LIMIT),
233
+ };
234
+ }
235
+ function frontierReportedChanges(ledger) {
236
+ if (ledger.openTurn !== undefined)
237
+ return summarizeReportedChanges(ledger.openTurn.rows);
238
+ const closed = ledger.turns.findLast((turn) => turn.kind === "closed");
239
+ return closed === undefined ? EMPTY_REPORTED : summarizeReportedChanges(closed.rows);
240
+ }
207
241
  /** Select one current Turn, one latest outcome, or no focus; pending tells stay actionable. */
208
242
  export function selectSnapshot(ledger, budget = { tail: DEFAULT_TAIL, voice: DEFAULT_VOICE }) {
209
- if (ledger.turns.length === 0 && !ledger.rows.some((row) => row.kind === "tell"))
210
- return { kind: "unborn", entries: [], omitted: 0 };
243
+ if (ledger.turns.length === 0 && !ledger.rows.some((row) => row.kind === "tell")) {
244
+ return { kind: "unborn", entries: [], omitted: 0, ...EMPTY_REPORTED };
245
+ }
211
246
  const pending = ledger.rows.filter((row) => row.kind === "tell" && row.state === "pending");
247
+ const reported = frontierReportedChanges(ledger);
212
248
  if (ledger.openTurn !== undefined) {
213
249
  const window = ledger.openTurn.rows;
214
250
  const active = window.filter((row) => row.kind === "tool" && row.state === "active");
@@ -222,13 +258,14 @@ export function selectSnapshot(ledger, budget = { tail: DEFAULT_TAIL, voice: DEF
222
258
  const voice = voiceCount === 0 ? [] : voiceCandidates.slice(-voiceCount);
223
259
  const selected = new Set([...tail, ...voice, ...active, ...pending]);
224
260
  const omitted = window.filter((row) => !selected.has(row)).length;
225
- return { kind: "open", turn: ledger.openTurn.turn, entries: openEntries(ledger, window, selected), omitted };
261
+ return { kind: "open", turn: ledger.openTurn.turn, entries: openEntries(ledger, window, selected), omitted, ...reported };
226
262
  }
227
263
  return {
228
264
  kind: "idle",
229
265
  ...(ledger.latestOutcome === undefined ? {} : { outcome: ledger.latestOutcome }),
230
266
  entries: entries(pending),
231
267
  omitted: 0,
268
+ ...reported,
232
269
  };
233
270
  }
234
271
  /** Apply the one snapshot policy used by every public observation consumer. */
@@ -23,7 +23,7 @@ export type ToolCall = Readonly<{
23
23
  }> | Readonly<{
24
24
  kind: "fileChange";
25
25
  changes: readonly Readonly<{
26
- op: "add" | "update" | "delete";
26
+ op: "add" | "update" | "delete" | "unspecified";
27
27
  path: string;
28
28
  diffstat?: Readonly<{
29
29
  added: number;
@@ -87,7 +87,8 @@ function decodeDiffstat(value) {
87
87
  }
88
88
  function decodeFileChangeMember(value) {
89
89
  const change = object(value);
90
- if (change === null || (change.op !== "add" && change.op !== "update" && change.op !== "delete")
90
+ if (change === null || (change.op !== "add" && change.op !== "update"
91
+ && change.op !== "delete" && change.op !== "unspecified")
91
92
  || typeof change.path !== "string")
92
93
  return null;
93
94
  const diffstat = decodeDiffstat(change.diffstat);
@@ -1,9 +1,12 @@
1
1
  import * as acp from "@agentclientprotocol/sdk";
2
2
  import { spawnStdioProcess } from "../../../runtime/proc/stdio.js";
3
3
  import { type ProviderAdapter, type Session } from "../../provider.js";
4
+ import { type AcpToolInterpreter, type AcpToolUpdate } from "./events.js";
5
+ export type { AcpToolInterpreter, AcpToolUpdate };
4
6
  export type AcpStartInput = Parameters<ProviderAdapter["start"]>[0] | Parameters<NonNullable<ProviderAdapter["resume"]>>[0];
5
7
  export type AcpDependencies = Readonly<{
6
8
  spawnProcess?: typeof spawnStdioProcess;
9
+ interpretTool?: AcpToolInterpreter;
7
10
  }>;
8
11
  export type AcpLiveSession = Readonly<{
9
12
  session: Session;
@@ -16,4 +19,3 @@ type AcpLaunch = Readonly<{
16
19
  env?: Readonly<Record<string, string>>;
17
20
  }>;
18
21
  export declare function startAcpSession(launch: AcpLaunch, input: AcpStartInput, dependencies?: AcpDependencies): Promise<AcpLiveSession>;
19
- export {};
@@ -3,7 +3,7 @@ import { Readable, Writable } from "node:stream";
3
3
  import { spawnStdioProcess } from "../../../runtime/proc/stdio.js";
4
4
  import { abortable } from "../../abort.js";
5
5
  import { AgentEventChannel, } from "../../provider.js";
6
- import { EMPTY_ACP_EVENT_STATE, flushAcpEvents, mapAcpUpdate } from "./events.js";
6
+ import { EMPTY_ACP_EVENT_STATE, flushAcpEvents, mapAcpUpdate, } from "./events.js";
7
7
  function diagnostic(error) {
8
8
  if (!(error instanceof Error))
9
9
  return String(error);
@@ -46,7 +46,7 @@ async function establishSession(agent, input) {
46
46
  await agent.request(acp.methods.agent.session.load, { cwd: input.cwd, mcpServers: [], sessionId });
47
47
  return sessionId;
48
48
  }
49
- function createAcpTurn(connection) {
49
+ function createAcpTurn(connection, interpret) {
50
50
  const events = new AgentEventChannel();
51
51
  let state = EMPTY_ACP_EVENT_STATE;
52
52
  let terminal = false;
@@ -55,7 +55,7 @@ function createAcpTurn(connection) {
55
55
  const update = (next) => {
56
56
  if (terminal)
57
57
  return;
58
- const mapped = mapAcpUpdate(next, state);
58
+ const mapped = mapAcpUpdate(next, state, interpret);
59
59
  state = mapped.state;
60
60
  for (const event of mapped.events)
61
61
  events.emit(event);
@@ -138,7 +138,7 @@ export async function startAcpSession(launch, input, dependencies = {}) {
138
138
  if (notification.sessionId === sessionId)
139
139
  turn.update(notification.update);
140
140
  }).connect(acp.ndJsonStream(Writable.toWeb(child.input), Readable.toWeb(child.output)));
141
- turn = createAcpTurn(connection);
141
+ turn = createAcpTurn(connection, dependencies.interpretTool);
142
142
  try {
143
143
  sessionId = await abortable(establishSession(connection.agent, input), signal);
144
144
  turn.events.emit({ type: "session", coordinate: { sessionId } });
@@ -1,14 +1,22 @@
1
1
  import type { SessionUpdate } from "@agentclientprotocol/sdk";
2
- import { type AgentEvent } from "../../provider.js";
2
+ import { type AgentEvent, type ToolCall } from "../../provider.js";
3
3
  type OpenBlock = Readonly<{
4
4
  type: "assistant" | "thought";
5
5
  text: string;
6
6
  }> | null;
7
+ export type AcpToolObservation = Readonly<{
8
+ name: string;
9
+ call: ToolCall;
10
+ }>;
11
+ export type AcpToolUpdate = Extract<SessionUpdate, {
12
+ sessionUpdate: "tool_call" | "tool_call_update";
13
+ }>;
14
+ export type AcpToolInterpreter = (update: AcpToolUpdate) => ToolCall | undefined;
7
15
  export type AcpEventState = Readonly<{
8
16
  answer: string;
9
17
  assistantMessageId: string | null;
10
18
  open: OpenBlock;
11
- tools: ReadonlyMap<string, string>;
19
+ tools: ReadonlyMap<string, AcpToolObservation>;
12
20
  }>;
13
21
  type AcpEventMapping = Readonly<{
14
22
  events: readonly AgentEvent[];
@@ -16,5 +24,5 @@ type AcpEventMapping = Readonly<{
16
24
  }>;
17
25
  export declare const EMPTY_ACP_EVENT_STATE: AcpEventState;
18
26
  export declare function flushAcpEvents(state: AcpEventState, boundary?: readonly AgentEvent[]): AcpEventMapping;
19
- export declare function mapAcpUpdate(update: SessionUpdate, previous: AcpEventState): AcpEventMapping;
27
+ export declare function mapAcpUpdate(update: SessionUpdate, previous: AcpEventState, interpret?: AcpToolInterpreter): AcpEventMapping;
20
28
  export {};
@@ -33,7 +33,82 @@ function appendBlock(state, type, text, messageId) {
33
33
  },
34
34
  };
35
35
  }
36
- export function mapAcpUpdate(update, previous) {
36
+ function nonblank(value) {
37
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
38
+ }
39
+ function object(value) {
40
+ return value !== null && typeof value === "object" && !Array.isArray(value)
41
+ ? value
42
+ : undefined;
43
+ }
44
+ function positiveLine(value) {
45
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 1
46
+ ? value
47
+ : undefined;
48
+ }
49
+ function otherCall(name) {
50
+ return { kind: "other", display: name };
51
+ }
52
+ function locationFact(locations) {
53
+ if (locations === null || locations === undefined)
54
+ return {};
55
+ for (const location of locations) {
56
+ const path = nonblank(location.path);
57
+ if (path === undefined)
58
+ continue;
59
+ const offset = positiveLine(location.line);
60
+ return offset === undefined ? { path } : { path, offset };
61
+ }
62
+ return {};
63
+ }
64
+ function standardCall(update, name) {
65
+ const location = locationFact(update.locations);
66
+ if (update.kind === "read") {
67
+ return location.path === undefined
68
+ ? otherCall(name)
69
+ : {
70
+ kind: "read",
71
+ path: location.path,
72
+ ...(location.offset === undefined ? {} : { offset: location.offset }),
73
+ };
74
+ }
75
+ const input = object(update.rawInput);
76
+ if (update.kind === "execute") {
77
+ const command = nonblank(input?.command);
78
+ return command === undefined ? otherCall(name) : { kind: "run", command };
79
+ }
80
+ if (update.kind === "search") {
81
+ const query = nonblank(input?.query);
82
+ if (query === undefined)
83
+ return otherCall(name);
84
+ const path = nonblank(input?.path);
85
+ const glob = nonblank(input?.glob);
86
+ const scope = input?.scope;
87
+ const searchScope = scope === "content" || scope === "files" || scope === "web"
88
+ ? scope
89
+ : undefined;
90
+ return {
91
+ kind: "search",
92
+ query,
93
+ ...(searchScope === undefined ? {} : { scope: searchScope }),
94
+ ...(path === undefined ? {} : { path }),
95
+ ...(glob === undefined ? {} : { glob }),
96
+ };
97
+ }
98
+ return otherCall(name);
99
+ }
100
+ function strongest(previous, next) {
101
+ if (previous === undefined || previous.kind === "other")
102
+ return next;
103
+ return next.kind === "other" ? previous : next;
104
+ }
105
+ function observeTool(update, previous, interpret) {
106
+ const name = nonblank(update.name) ?? nonblank(update.title) ?? previous?.name ?? "ACP tool";
107
+ const standard = standardCall(update, name);
108
+ const dialect = standard.kind === "other" ? interpret?.(update) : undefined;
109
+ return { name, call: strongest(previous?.call, dialect ?? standard) };
110
+ }
111
+ export function mapAcpUpdate(update, previous, interpret) {
37
112
  switch (update.sessionUpdate) {
38
113
  case "agent_message_chunk":
39
114
  case "agent_thought_chunk": {
@@ -47,15 +122,14 @@ export function mapAcpUpdate(update, previous) {
47
122
  case "user_message_chunk": return flushAcpEvents(previous);
48
123
  case "tool_call":
49
124
  case "tool_call_update": {
50
- const name = update.title ?? (update.sessionUpdate === "tool_call" ? undefined : previous.tools.get(update.toolCallId)) ?? "ACP tool";
51
- const observed = { name, call: { kind: "other", display: name } };
125
+ const observed = observeTool(update, previous.tools.get(update.toolCallId), interpret);
52
126
  const tools = new Map(previous.tools);
53
127
  if (update.status === "completed" || update.status === "failed") {
54
128
  tools.delete(update.toolCallId);
55
129
  return flushAcpEvents({ ...previous, tools }, [{ type: "tool", phase: "completed", id: update.toolCallId, ...observed, result: { status: update.status === "completed" ? "ok" : "error" } }]);
56
130
  }
57
131
  const started = tools.has(update.toolCallId);
58
- tools.set(update.toolCallId, name);
132
+ tools.set(update.toolCallId, observed);
59
133
  return started
60
134
  ? flushAcpEvents({ ...previous, tools })
61
135
  : flushAcpEvents({ ...previous, tools }, [{ type: "tool", phase: "started", id: update.toolCallId, ...observed }]);
@@ -141,12 +141,63 @@ function fileChangeCall(name, path) {
141
141
  }
142
142
  return undefined;
143
143
  }
144
+ function startedPath(name, value) {
145
+ if (name === "NotebookEdit")
146
+ return nonblank(value.notebook_path);
147
+ return nonblank(value.file_path) ?? (name === "Read" ? nonblank(value.path) : undefined);
148
+ }
149
+ function nonnegativeInt(value) {
150
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : undefined;
151
+ }
152
+ function fileChange(op, path, gitDiff) {
153
+ const diff = object(gitDiff);
154
+ const added = nonnegativeInt(diff?.additions);
155
+ const removed = nonnegativeInt(diff?.deletions);
156
+ const diffstat = added === undefined || removed === undefined ? undefined : { added, removed };
157
+ return {
158
+ kind: "fileChange",
159
+ changes: [{ op, path, ...(diffstat === undefined ? {} : { diffstat }) }],
160
+ };
161
+ }
162
+ function writeResult(result) {
163
+ const value = object(result);
164
+ if (value === undefined || (value.type !== "create" && value.type !== "update"))
165
+ return undefined;
166
+ const path = nonblank(value.filePath);
167
+ if (path === undefined)
168
+ return undefined;
169
+ const op = value.type === "create" ? "add" : "update";
170
+ return fileChange(op, path, value.gitDiff);
171
+ }
172
+ function editResult(result) {
173
+ const value = object(result);
174
+ if (value === undefined || value.type === "create" || value.type === "update")
175
+ return undefined;
176
+ const path = nonblank(value.filePath);
177
+ if (path === undefined)
178
+ return undefined;
179
+ const status = object(value.gitDiff)?.status;
180
+ return fileChange(status === "added" ? "add" : "update", path, value.gitDiff);
181
+ }
182
+ function notebookResult(result) {
183
+ const path = nonblank(object(result)?.notebook_path);
184
+ return path === undefined
185
+ ? undefined
186
+ : { kind: "fileChange", changes: [{ op: "update", path }] };
187
+ }
188
+ function structuredFileChange(name, result) {
189
+ if (name === "Write")
190
+ return writeResult(result);
191
+ if (name === "Edit")
192
+ return editResult(result);
193
+ return name === "NotebookEdit" ? notebookResult(result) : undefined;
194
+ }
144
195
  function toolCall(name, input) {
145
196
  const value = object(input) ?? {};
146
197
  const run = runCall(name, value);
147
198
  if (run !== undefined)
148
199
  return run;
149
- const path = nonblank(value.file_path) ?? (name === "Read" ? nonblank(value.path) : undefined);
200
+ const path = startedPath(name, value);
150
201
  return readCall(name, path, value)
151
202
  ?? searchCall(name, value)
152
203
  ?? fileChangeCall(name, path)
@@ -192,9 +243,17 @@ function toolResultEvents(message, events, state) {
192
243
  if (observed === undefined)
193
244
  continue;
194
245
  state.tools.delete(id);
246
+ const failed = value.is_error === true;
247
+ const structured = failed
248
+ ? undefined
249
+ : structuredFileChange(observed.name, message.tool_use_result);
195
250
  events.emit({
196
- type: "tool", phase: "completed", id, name: observed.name, call: observed.call,
197
- result: { status: value.is_error === true ? "error" : "ok" },
251
+ type: "tool",
252
+ phase: "completed",
253
+ id,
254
+ name: observed.name,
255
+ call: structured ?? observed.call,
256
+ result: { status: failed ? "error" : "ok" },
198
257
  });
199
258
  }
200
259
  }
@@ -1,4 +1,5 @@
1
1
  import { noteEvent, unknownEvent, } from "../../provider.js";
2
+ import { diffstatFromUnifiedPatch } from "../unified-patch.js";
2
3
  export const CODEX_NOTIFICATION_DISPOSITIONS = {
3
4
  "account/login/completed": "drop",
4
5
  "account/rateLimits/updated": "drop",
@@ -135,19 +136,6 @@ function fileChangeOp(value) {
135
136
  ? record.type
136
137
  : undefined;
137
138
  }
138
- function diffstatFromUnifiedPatch(patch) {
139
- if (!patch.split("\n").some((line) => /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/u.test(line)))
140
- return undefined;
141
- let added = 0;
142
- let removed = 0;
143
- for (const line of patch.split("\n")) {
144
- if (line.startsWith("+") && !line.startsWith("+++"))
145
- added += 1;
146
- if (line.startsWith("-") && !line.startsWith("---"))
147
- removed += 1;
148
- }
149
- return { added, removed };
150
- }
151
139
  function itemChanges(item) {
152
140
  if (!Array.isArray(item.changes))
153
141
  return [];
@@ -1,4 +1,5 @@
1
1
  import type { ProviderAdapter } from "../../provider.js";
2
2
  import type { ProviderExecution } from "../../provider-recipe.js";
3
- import { type AcpDependencies } from "../acp/core.js";
3
+ import { type AcpDependencies, type AcpToolInterpreter } from "../acp/core.js";
4
+ export declare const interpretGrokTool: AcpToolInterpreter;
4
5
  export declare function createGrokBuildProvider(execution: ProviderExecution, dependencies?: AcpDependencies): ProviderAdapter;