@adecore/agents 0.21.1 → 0.22.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.
Files changed (121) hide show
  1. package/dist/async.js +3 -8
  2. package/dist/chat/backend.d.ts +4 -0
  3. package/dist/chat/chat-core.d.ts +5 -2
  4. package/dist/chat/chat-core.js +8 -7
  5. package/dist/chat/chat-log.js +2 -2
  6. package/dist/chat/chat-session.d.ts +11 -2
  7. package/dist/chat/chat-session.js +141 -13
  8. package/dist/chat/chat-store.js +4 -7
  9. package/dist/chat/chat-title.js +4 -8
  10. package/dist/chat/claude-backend.d.ts +1 -0
  11. package/dist/chat/claude-backend.js +8 -6
  12. package/dist/chat/claude-protocol.d.ts +2 -0
  13. package/dist/chat/claude-protocol.js +21 -1
  14. package/dist/chat/claude-title.js +1 -1
  15. package/dist/chat/claude-transcript.d.ts +1 -0
  16. package/dist/chat/claude-transcript.js +1 -1
  17. package/dist/chat/codex-backend.d.ts +2 -2
  18. package/dist/chat/codex-backend.js +5 -8
  19. package/dist/chat/codex-protocol.js +1 -1
  20. package/dist/chat/codex-thread.js +4 -5
  21. package/dist/chat/composer-preferences.js +2 -3
  22. package/dist/chat/errors.d.ts +1 -1
  23. package/dist/chat/external-sessions.d.ts +26 -0
  24. package/dist/chat/external-sessions.js +136 -0
  25. package/dist/chat/fake-claude.js +73 -0
  26. package/dist/chat/limit-resume.js +2 -7
  27. package/dist/chat/thread-search.d.ts +14 -0
  28. package/dist/chat/thread-search.js +64 -0
  29. package/dist/chat/visual-store.js +8 -6
  30. package/dist/context/refusal.js +3 -7
  31. package/dist/context/verb.js +1 -5
  32. package/dist/host/agent-host.js +2 -2
  33. package/dist/host/handlers.js +1 -2
  34. package/dist/host/wiring.js +1 -1
  35. package/dist/lineage.js +2 -3
  36. package/dist/messages/deliver-message.js +6 -11
  37. package/dist/messages/deliver-notice.js +8 -12
  38. package/dist/messages/notice-store.js +3 -5
  39. package/dist/outbox/outbox-worker.js +5 -2
  40. package/dist/providers/accounts/service.d.ts +2 -1
  41. package/dist/providers/accounts/service.js +11 -13
  42. package/dist/providers/catalog.js +2 -1
  43. package/dist/providers/claude.js +0 -1
  44. package/dist/providers/detect.js +2 -10
  45. package/dist/record-directory.js +0 -1
  46. package/dist/tasks/task-coordinator.d.ts +1 -0
  47. package/dist/tasks/task-coordinator.js +34 -34
  48. package/dist/tasks/task-store.js +1 -1
  49. package/dist/tasks/task-work.js +2 -2
  50. package/dist/tasks/waiting-child.d.ts +1 -0
  51. package/dist/tasks/waiting-child.js +8 -6
  52. package/dist/tasks/wake-parent.js +15 -10
  53. package/dist/tasks/wiring.js +32 -37
  54. package/dist/usage/aggregate.js +62 -46
  55. package/dist/usage/exchange.js +1 -1
  56. package/dist/usage/limits/monitor.d.ts +2 -2
  57. package/dist/usage/limits/monitor.js +2 -2
  58. package/dist/usage/limits/probe.js +1 -1
  59. package/dist/usage/pricing.js +1 -1
  60. package/dist/usage/readers/codex.d.ts +1 -0
  61. package/dist/usage/readers/codex.js +1 -1
  62. package/dist/usage/roots.js +1 -2
  63. package/dist/usage/scanner.js +7 -9
  64. package/dist/usage/usage-service.d.ts +1 -2
  65. package/dist/usage/usage-service.js +3 -6
  66. package/package.json +3 -3
  67. package/src/async.ts +3 -9
  68. package/src/chat/backend.ts +2 -0
  69. package/src/chat/chat-core.ts +8 -7
  70. package/src/chat/chat-log.ts +2 -2
  71. package/src/chat/chat-session.ts +148 -13
  72. package/src/chat/chat-store.ts +4 -8
  73. package/src/chat/chat-title.ts +4 -8
  74. package/src/chat/claude-backend.ts +9 -6
  75. package/src/chat/claude-protocol.ts +23 -1
  76. package/src/chat/claude-title.ts +1 -1
  77. package/src/chat/claude-transcript.ts +2 -2
  78. package/src/chat/codex-backend.ts +5 -8
  79. package/src/chat/codex-protocol.ts +1 -1
  80. package/src/chat/codex-thread.ts +6 -5
  81. package/src/chat/composer-preferences.ts +2 -3
  82. package/src/chat/errors.ts +2 -0
  83. package/src/chat/external-sessions.ts +177 -0
  84. package/src/chat/fake-claude.ts +75 -1
  85. package/src/chat/limit-resume.ts +2 -7
  86. package/src/chat/thread-search.ts +87 -0
  87. package/src/chat/thread.ts +1 -1
  88. package/src/chat/visual-store.ts +9 -6
  89. package/src/chat/wake-chat.ts +4 -1
  90. package/src/context/refusal.ts +3 -7
  91. package/src/context/verb.ts +1 -5
  92. package/src/host/agent-host.ts +2 -2
  93. package/src/host/handlers.ts +1 -2
  94. package/src/host/wiring.ts +1 -1
  95. package/src/lineage.ts +2 -3
  96. package/src/messages/deliver-message.ts +6 -11
  97. package/src/messages/deliver-notice.ts +8 -12
  98. package/src/messages/notice-store.ts +3 -5
  99. package/src/outbox/outbox-worker.ts +5 -2
  100. package/src/providers/accounts/service.ts +12 -13
  101. package/src/providers/accounts/variables.ts +1 -0
  102. package/src/providers/catalog.ts +2 -1
  103. package/src/providers/claude.ts +0 -1
  104. package/src/providers/detect.ts +2 -11
  105. package/src/record-directory.ts +0 -1
  106. package/src/skills.ts +0 -1
  107. package/src/tasks/task-coordinator.ts +35 -33
  108. package/src/tasks/task-store.ts +1 -1
  109. package/src/tasks/task-work.ts +2 -2
  110. package/src/tasks/waiting-child.ts +10 -7
  111. package/src/tasks/wake-parent.ts +21 -16
  112. package/src/tasks/wiring.ts +33 -41
  113. package/src/usage/aggregate.ts +80 -50
  114. package/src/usage/exchange.ts +1 -1
  115. package/src/usage/limits/monitor.ts +4 -4
  116. package/src/usage/limits/probe.ts +2 -2
  117. package/src/usage/pricing.ts +1 -1
  118. package/src/usage/readers/codex.ts +1 -1
  119. package/src/usage/roots.ts +1 -2
  120. package/src/usage/scanner.ts +8 -10
  121. package/src/usage/usage-service.ts +5 -9
@@ -28,6 +28,59 @@ function slotOf(format, timestampMs, resolution) {
28
28
  function emptyTotals() {
29
29
  return { ...EMPTY_TOTALS };
30
30
  }
31
+ function foldBucket(buckets, slot, record, account, price, cost) {
32
+ const key = `${slot}\0${record.provider}\0${record.model}\0${account ?? ''}`;
33
+ let bucket = buckets.get(key);
34
+ if (bucket === undefined) {
35
+ bucket = {
36
+ slot,
37
+ provider: record.provider,
38
+ model: record.model,
39
+ account,
40
+ totals: emptyTotals(),
41
+ costUsd: 0,
42
+ priced: false,
43
+ cacheSavingsUsd: 0,
44
+ sessions: new Set()
45
+ };
46
+ buckets.set(key, bucket);
47
+ }
48
+ bucket.totals = addTotals(bucket.totals, record.totals);
49
+ bucket.costUsd += cost;
50
+ bucket.priced ||= price !== null;
51
+ bucket.cacheSavingsUsd += price === null ? 0 : cacheSavingsOf(record.totals, price);
52
+ if (record.sessionId !== '') {
53
+ bucket.sessions.add(record.sessionId);
54
+ }
55
+ }
56
+ function foldModel(models, record, account, lookup, cost) {
57
+ const key = `${record.provider}\0${record.model}\0${account ?? ''}`;
58
+ let model = models.get(key);
59
+ if (model === undefined) {
60
+ model = {
61
+ provider: record.provider,
62
+ model: record.model,
63
+ account,
64
+ totals: emptyTotals(),
65
+ costUsd: 0,
66
+ priced: false,
67
+ basis: lookup.basis,
68
+ pricedAs: lookup.pricedAs
69
+ };
70
+ models.set(key, model);
71
+ }
72
+ model.totals = addTotals(model.totals, record.totals);
73
+ model.costUsd += cost;
74
+ model.priced ||= lookup.price !== null;
75
+ }
76
+ function foldProject(project, record, cost) {
77
+ project.totals = addTotals(project.totals, record.totals);
78
+ project.costUsd += cost;
79
+ const share = project.byProvider.get(record.provider) ?? { costUsd: 0, tokens: 0 };
80
+ share.costUsd += cost;
81
+ share.tokens += totalTokensOf(record.totals);
82
+ project.byProvider.set(record.provider, share);
83
+ }
31
84
  /* The account a record was made under, the default one under its CLI's kind. */
32
85
  export function accountOfRecord(record) {
33
86
  return record.account ?? record.provider;
@@ -41,8 +94,6 @@ export async function aggregate(records, payload, prices, known) {
41
94
  const format = slotFormatter(payload.timeZone, payload.resolution);
42
95
  const buckets = new Map();
43
96
  const models = new Map();
44
- /* The basis of the first record of a model prices the whole row: a model has one price. */
45
- const modelBasis = new Map();
46
97
  const projects = new Map();
47
98
  const sessions = new Set();
48
99
  const resolver = new ProjectResolver(known);
@@ -59,42 +110,13 @@ export async function aggregate(records, payload, prices, known) {
59
110
  if (day < payload.from || day > payload.to) {
60
111
  continue;
61
112
  }
62
- const { price, basis, pricedAs } = prices.look(record.model);
63
- const cost = price === null ? 0 : costOf(record.totals, price);
64
- const bucketKey = `${slot}\0${record.provider}\0${record.model}\0${account ?? ''}`;
65
- let bucket = buckets.get(bucketKey);
66
- if (bucket === undefined) {
67
- bucket = {
68
- slot,
69
- provider: record.provider,
70
- model: record.model,
71
- account,
72
- totals: emptyTotals(),
73
- costUsd: 0,
74
- priced: false,
75
- cacheSavingsUsd: 0,
76
- sessions: new Set()
77
- };
78
- buckets.set(bucketKey, bucket);
79
- }
80
- bucket.totals = addTotals(bucket.totals, record.totals);
81
- bucket.costUsd += cost;
82
- bucket.priced ||= price !== null;
83
- bucket.cacheSavingsUsd += price === null ? 0 : cacheSavingsOf(record.totals, price);
113
+ const lookup = prices.look(record.model);
114
+ const cost = lookup.price === null ? 0 : costOf(record.totals, lookup.price);
115
+ foldBucket(buckets, slot, record, account, lookup.price, cost);
116
+ foldModel(models, record, account, lookup, cost);
84
117
  if (record.sessionId !== '') {
85
- bucket.sessions.add(record.sessionId);
86
118
  sessions.add(`${record.provider}\0${record.sessionId}`);
87
119
  }
88
- const modelKey = `${record.provider}\0${record.model}\0${account ?? ''}`;
89
- let model = models.get(modelKey);
90
- if (model === undefined) {
91
- model = { provider: record.provider, model: record.model, account, totals: emptyTotals(), costUsd: 0, priced: false };
92
- models.set(modelKey, model);
93
- modelBasis.set(modelKey, { basis, pricedAs });
94
- }
95
- model.totals = addTotals(model.totals, record.totals);
96
- model.costUsd += cost;
97
- model.priced ||= price !== null;
98
120
  let folder = folders.get(record.cwd);
99
121
  if (folder === undefined) {
100
122
  const resolved = await resolver.resolve(record.cwd);
@@ -104,13 +126,7 @@ export async function aggregate(records, payload, prices, known) {
104
126
  projects.set(folder, { folder, name: resolved.name, projectId: resolved.projectId, byProvider: new Map(), totals: emptyTotals(), costUsd: 0 });
105
127
  }
106
128
  }
107
- const project = projects.get(folder);
108
- project.totals = addTotals(project.totals, record.totals);
109
- project.costUsd += cost;
110
- const share = project.byProvider.get(record.provider) ?? { costUsd: 0, tokens: 0 };
111
- share.costUsd += cost;
112
- share.tokens += totalTokensOf(record.totals);
113
- project.byProvider.set(record.provider, share);
129
+ foldProject(projects.get(folder), record, cost);
114
130
  }
115
131
  return {
116
132
  buckets: [...buckets.values()]
@@ -128,15 +144,15 @@ export async function aggregate(records, payload, prices, known) {
128
144
  a.provider.localeCompare(b.provider) ||
129
145
  a.model.localeCompare(b.model) ||
130
146
  (a.account ?? '').localeCompare(b.account ?? '')),
131
- models: [...models.entries()]
132
- .map(([key, model]) => ({
147
+ models: [...models.values()]
148
+ .map((model) => ({
133
149
  provider: model.provider,
134
150
  model: model.model,
135
151
  ...(model.account === null ? {} : { account: model.account }),
136
152
  totals: model.totals,
137
153
  costUsd: model.priced ? model.costUsd : null,
138
- priceBasis: modelBasis.get(key)?.basis ?? 'unknown',
139
- pricedAs: modelBasis.get(key)?.pricedAs ?? null
154
+ priceBasis: model.basis,
155
+ pricedAs: model.pricedAs
140
156
  }))
141
157
  .sort((a, b) => (b.costUsd ?? -1) - (a.costUsd ?? -1)),
142
158
  projects: [...projects.values()]
@@ -1,7 +1,7 @@
1
1
  import { mkdir, readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
- import { isNotFound, writeAtomic } from '../fs.js';
4
3
  import { errorText } from '../error-text.js';
4
+ import { isNotFound, writeAtomic } from '../fs.js';
5
5
  export const FRANKFURTER_URL = 'https://api.frankfurter.app/latest';
6
6
  /* The bank publishes one rate a day, so a rate a day old is the rate. */
7
7
  const TTL_MS = 24 * 60 * 60 * 1000;
@@ -1,8 +1,8 @@
1
1
  import type { UsageLimitsSnapshot, UsageProvider } from '@adecore/agent-contracts';
2
- import type { ProviderRegistry } from '../../providers/registry.ts';
2
+ import type { CodexClientInfo } from '../../chat/codex-transport.ts';
3
3
  import type { AgentSink } from '../../events.ts';
4
+ import type { ProviderRegistry } from '../../providers/registry.ts';
4
5
  import { type LimitsUpdate } from './normalize.ts';
5
- import type { CodexClientInfo } from '../../chat/codex-transport.ts';
6
6
  import { type ProbeResult } from './probe.ts';
7
7
  export type ProbeEnv = Record<string, string>;
8
8
  export interface LimitAccount {
@@ -1,8 +1,8 @@
1
+ import { ClientSinks } from '../../client-sinks.js';
2
+ import { errorText } from '../../error-text.js';
1
3
  import { definedEnv } from '../../providers/accounts/launch.js';
2
4
  import { mergeWindows } from './normalize.js';
3
5
  import { probeClaude, probeCodex } from './probe.js';
4
- import { ClientSinks } from '../../client-sinks.js';
5
- import { errorText } from '../../error-text.js';
6
6
  /* Often enough that a bar is never far behind, rarely enough that two CLIs are not started all day. */
7
7
  const PROBE_INTERVAL_MS = 5 * 60_000;
8
8
  const MAX_BACKOFF_MS = 60 * 60_000;
@@ -1,8 +1,8 @@
1
1
  import { withTimeout } from '../../async.js';
2
2
  import { spawnChatProcess } from '../../chat/chat-process.js';
3
3
  import { CodexTransport, DEFAULT_CODEX_CLIENT } from '../../chat/codex-transport.js';
4
- import { readClaudeUsage, readCodexLimits } from './normalize.js';
5
4
  import { errorText } from '../../error-text.js';
5
+ import { readClaudeUsage, readCodexLimits } from './normalize.js';
6
6
  /* A CLI that has not answered by now is not going to; the next pass tries again. */
7
7
  const PROBE_TIMEOUT_MS = 20_000;
8
8
  const CODEX_READ_TIMEOUT_MS = 3_000;
@@ -1,7 +1,7 @@
1
1
  import { mkdir, readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
- import { isNotFound, writeAtomic } from '../fs.js';
4
3
  import { errorText } from '../error-text.js';
4
+ import { isNotFound, writeAtomic } from '../fs.js';
5
5
  import bundled from './prices-snapshot.json' with { type: 'json' };
6
6
  export const LITELLM_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
7
7
  /* A table a day old still prices everything anyone ran today, and nobody has to be online for it. */
@@ -19,5 +19,6 @@ export interface CodexParserState {
19
19
  }
20
20
  export declare function createCodexState(): CodexParserState;
21
21
  export declare function cloneCodexState(state: CodexParserState): CodexParserState;
22
+ export declare function isForkOrSubagent(payload: Record<string, unknown>): boolean;
22
23
  export declare function parseCodexLine(line: string, state: CodexParserState): UsageRecord | null;
23
24
  export {};
@@ -45,7 +45,7 @@ function deltaOf(total, previous) {
45
45
  return delta.input < 0 || delta.cached < 0 || delta.cacheWrite < 0 || delta.output < 0 || delta.reasoning < 0 ? null : delta;
46
46
  }
47
47
  /* A thread that was forked or spawned repeats its parent's events before its own work starts. */
48
- function isForkOrSubagent(payload) {
48
+ export function isForkOrSubagent(payload) {
49
49
  if (typeof payload.forked_from_id === 'string' || typeof payload.parent_thread_id === 'string') {
50
50
  return true;
51
51
  }
@@ -2,8 +2,7 @@ import { homedir } from 'node:os';
2
2
  import { join } from 'node:path';
3
3
  /* Where in its config folder each CLI leaves its transcripts. */
4
4
  const TRANSCRIPTS = { claude: 'projects', codex: 'sessions' };
5
- /* Where each CLI leaves its transcripts. The same environment variables the hooks installer reads,
6
- so the scanner and the installer never disagree about where a CLI keeps its things. */
5
+ /* The transcripts of the default accounts, found through the variables the CLIs themselves read. */
7
6
  export function usageRoots(env = process.env) {
8
7
  const home = env.HOME ?? homedir();
9
8
  return [
@@ -1,12 +1,12 @@
1
1
  import { mkdir, open, readFile, readdir, stat } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
+ import { errorText } from '../error-text.js';
3
4
  import { isNotFound, writeAtomic } from '../fs.js';
4
5
  import { decodeIndex, encodeIndex } from './index-file.js';
5
6
  import { foldByKey } from './record.js';
6
7
  import { claudeMightCarryUsage, parseClaudeLine } from './readers/claude.js';
7
8
  import { cloneCodexState, codexMightCarryUsage, createCodexState, parseCodexLine } from './readers/codex.js';
8
9
  import { usageRoots } from './roots.js';
9
- import { errorText } from '../error-text.js';
10
10
  /* What a file holds from `from` to its end, as it is at the read. */
11
11
  async function readFrom(path, from) {
12
12
  const handle = await open(path, 'r');
@@ -53,11 +53,11 @@ async function listJsonl(dir, out = []) {
53
53
  * read again next time. The Codex state is snapshotted before that tail for the same reason.
54
54
  */
55
55
  function parseChunk(text, provider, from, state) {
56
- let records = [];
57
56
  const end = text.lastIndexOf('\n');
58
57
  const whole = end === -1 ? '' : text.slice(0, end);
59
58
  const tail = text.slice(end + 1);
60
59
  const read = (chunk, codex) => {
60
+ const records = [];
61
61
  for (const raw of chunk.split('\n')) {
62
62
  const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
63
63
  if (line === '') {
@@ -79,14 +79,12 @@ function parseChunk(text, provider, from, state) {
79
79
  }
80
80
  }
81
81
  }
82
+ return records;
82
83
  };
83
84
  const codex = provider === 'codex' ? (state ?? createCodexState()) : null;
84
- read(whole, codex);
85
+ const counted = read(whole, codex);
85
86
  const committed = codex === null ? null : cloneCodexState(codex);
86
- const counted = records;
87
- records = [];
88
- read(tail, codex);
89
- return { records: counted, tail: records, offset: from + (end === -1 ? 0 : Buffer.byteLength(whole, 'utf8') + 1), codex: committed };
87
+ return { records: counted, tail: read(tail, codex), offset: from + (end === -1 ? 0 : Buffer.byteLength(whole, 'utf8') + 1), codex: committed };
90
88
  }
91
89
  /*
92
90
  * The account a record of a root belongs to, as a record keeps it (absent for the default account):
@@ -113,8 +111,8 @@ function withAccount(records, accountOf) {
113
111
  });
114
112
  }
115
113
  /*
116
- * Read appended transcript bytes incrementally, but restart after rewrites or truncation. Preserve
117
- * totals for deleted transcripts because their usage still happened.
114
+ * Reads only what a transcript appended since the last scan, and starts a file over once it was
115
+ * rewritten or cut. A deleted transcript keeps its totals, since its usage still happened.
118
116
  */
119
117
  export class UsageScanner {
120
118
  home;
@@ -1,8 +1,8 @@
1
1
  import type { UsageAccount, UsageSummaryPayload, UsageSummaryResult } from '@adecore/agent-contracts';
2
2
  import type { AgentSink } from '../events.ts';
3
3
  import type { KnownProject } from './projects.ts';
4
- import { type UsageScannerOptions } from './scanner.ts';
5
4
  import type { UsageRootPath } from './roots.ts';
5
+ import { type UsageScannerOptions } from './scanner.ts';
6
6
  export interface UsageServiceOptions {
7
7
  home: string;
8
8
  allowPriceFetch?: boolean;
@@ -31,5 +31,4 @@ export declare class UsageService {
31
31
  stop(): void;
32
32
  private rescan;
33
33
  private runScan;
34
- private emit;
35
34
  }
@@ -1,9 +1,9 @@
1
+ import { ClientSinks } from '../client-sinks.js';
2
+ import { errorText } from '../error-text.js';
1
3
  import { accountOfRecord, aggregate } from './aggregate.js';
2
4
  import { ExchangeRates } from './exchange.js';
3
5
  import { PriceBook } from './pricing.js';
4
6
  import { UsageScanner } from './scanner.js';
5
- import { errorText } from '../error-text.js';
6
- import { ClientSinks } from '../client-sinks.js';
7
7
  /* A scan this fresh answers the question the page is asking, so nothing is opened for it. */
8
8
  const SCAN_TTL_MS = 60_000;
9
9
  /* The accounts of the machine, and after them any a record names that the machine no longer has, under its id. */
@@ -121,10 +121,7 @@ export class UsageService {
121
121
  }
122
122
  this.report = report;
123
123
  if (report.changedFiles > 0) {
124
- this.emit({ event: 'usage.changed', payload: { scannedAt: report.at } });
124
+ this.sinks.emit({ event: 'usage.changed', payload: { scannedAt: report.at } });
125
125
  }
126
126
  }
127
- emit(event) {
128
- this.sinks.emit(event);
129
- }
130
127
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/agents",
3
- "version": "0.21.1",
3
+ "version": "0.22.0",
4
4
  "description": "Node-compatible agent chat hosts, providers, accounts, usage and durable task coordination.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -20,8 +20,8 @@
20
20
  "test": "bun --conditions=source test"
21
21
  },
22
22
  "dependencies": {
23
- "@adecore/agent-contracts": "0.21.1",
24
- "@adecore/intelligent-ui": "0.21.1",
23
+ "@adecore/agent-contracts": "0.22.0",
24
+ "@adecore/intelligent-ui": "0.22.0",
25
25
  "zod": "^4.6.5"
26
26
  },
27
27
  "devDependencies": {
package/src/async.ts CHANGED
@@ -1,9 +1,6 @@
1
- /* The two waits the host does: a pause it can be woken from, and a deadline on somebody else's promise. */
2
-
3
1
  /*
4
- * Resolves after `ms`, or at once when the signal is raised. An abort ends the wait rather than
5
- * failing it: every caller here is a loop that checks the signal itself on the next turn, and a
6
- * rejection would only be caught and thrown away.
2
+ * Resolves after `ms`, or at once when the signal is raised. An abort resolves instead of rejecting:
3
+ * every caller is a loop that checks the signal itself on its next turn.
7
4
  */
8
5
  export function wait(ms: number, signal?: AbortSignal): Promise<void> {
9
6
  return new Promise((resolve) => {
@@ -19,10 +16,7 @@ export function wait(ms: number, signal?: AbortSignal): Promise<void> {
19
16
  });
20
17
  }
21
18
 
22
- /*
23
- * The promise, or `message` when it takes longer than `ms`. The timer is cleared either way, so a
24
- * call that answers at once does not hold the process for the rest of its deadline.
25
- */
19
+ /* The timer is cleared either way, so a call that answers at once does not hold the process until its deadline. */
26
20
  export function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
27
21
  return new Promise((resolve, reject) => {
28
22
  const timer = setTimeout(() => reject(new Error(message)), ms);
@@ -160,6 +160,8 @@ export type BackendEvent =
160
160
  | { type: 'compaction'; preTokens: number | null }
161
161
  | { type: 'model'; model: string }
162
162
  | { type: 'note'; level: 'info' | 'warning' | 'error'; text: string }
163
+ // The CLI answered a `stopTask` with an error, so the task still runs.
164
+ | { type: 'stop.refused'; taskId: string; message: string }
163
165
  // `native` is the CLI's own name for where the turn ended, which a fork of the chat is cut at.
164
166
  | {
165
167
  type: 'turn.done';
@@ -27,7 +27,6 @@ import {
27
27
  type ModelSelection,
28
28
  type RuntimeMode
29
29
  } from '@adecore/agent-contracts';
30
- import { ChatUiQueries, type ChatUiHost } from './ui-queries.ts';
31
30
  import { ClientSinks } from '../client-sinks.ts';
32
31
  import { errorText } from '../error-text.ts';
33
32
  import type { AgentEvent, AgentSink } from '../events.ts';
@@ -38,6 +37,7 @@ import { SkillIndex } from '../skills.ts';
38
37
  import type { LimitsUpdate } from '../usage/limits/normalize.ts';
39
38
  import { usageRoots } from '../usage/roots.ts';
40
39
  import type { AttachmentStore } from './attachment-store.ts';
40
+ import { commandLabel, isBackgroundWork, runsInBackground, type BackgroundWork } from './background-work.ts';
41
41
  import type { BookmarkStore } from './bookmark-store.ts';
42
42
  import { ChatLog, COMPACT_ABOVE_BYTES } from './chat-log.ts';
43
43
  import type { SpawnChatProcess } from './chat-process.ts';
@@ -57,10 +57,10 @@ import type { CodexProcessSpec } from './codex-thread.ts';
57
57
  import type { CodexClientInfo } from './codex-transport.ts';
58
58
  import { ComposerPreferences } from './composer-preferences.ts';
59
59
  import { DeltaCoalescer } from './delta-coalescer.ts';
60
- import { readableAssistantText } from './readable-text.ts';
61
60
  import { ChatError } from './errors.ts';
62
- import { commandLabel, isBackgroundWork, runsInBackground, type BackgroundWork } from './background-work.ts';
61
+ import { readableAssistantText } from './readable-text.ts';
63
62
  import { SubagentReader, type SubagentReaderOptions } from './subagent-reader.ts';
63
+ import { ChatUiQueries, type ChatUiHost } from './ui-queries.ts';
64
64
  import type { VisualInput, VisualStore } from './visual-store.ts';
65
65
 
66
66
  /* A turn that was running when the host went down, and the attempt that would take it up again. */
@@ -72,12 +72,12 @@ export interface InterruptedRun {
72
72
 
73
73
  type BookmarkableItem = Extract<ChatItem, { kind: 'user' | 'assistant' }>;
74
74
 
75
- /* A message a person or the agent wrote in the chat's own thread; a subagent's words belong to its row. */
76
75
  function withoutHidden(info: ChatInfo): ChatInfo {
77
76
  const { hidden: _hidden, ...rest } = info;
78
77
  return rest;
79
78
  }
80
79
 
80
+ /* A message a person or the agent wrote in the chat's own thread; a subagent's words belong to its row. */
81
81
  function isBookmarkable(item: ChatItem): item is BookmarkableItem {
82
82
  return item.kind === 'user' || (item.kind === 'assistant' && (item.parentToolUseId ?? null) === null);
83
83
  }
@@ -640,8 +640,9 @@ export class ChatCore {
640
640
  }
641
641
 
642
642
  /*
643
- * Stops one running sub-agent of the CLI's own. It cannot be stopped apart from its CLI, so its row
644
- * is only marked, and only while no turn runs: stopping that turn is the composer's Stop.
643
+ * Stops one running sub-agent of the CLI's own. The process that started it ends it with `stop_task`
644
+ * and the parent keeps working. Without such a process the row is only marked, and only while no
645
+ * turn runs: stopping that turn is the composer's Stop.
645
646
  */
646
647
  async stopSubagent(chatId: string, toolUseId: string): Promise<void> {
647
648
  await this.loaded(chatId);
@@ -656,7 +657,7 @@ export class ChatCore {
656
657
  if (row.origin === 'ruimte') {
657
658
  throw new ChatError('chat-unsupported', 'This row stands for work the host runs; stop it there');
658
659
  }
659
- session.markSubagentStopped(toolUseId);
660
+ session.stopSubagent(toolUseId);
660
661
  }
661
662
 
662
663
  /* Ends a command or a monitor a chat's CLI runs in the background, for a person who pressed Stop on it. */
@@ -48,8 +48,8 @@ export interface ChatLogState {
48
48
  }
49
49
 
50
50
  /*
51
- * Number and synchronously append each broadcast event so disk, memory and snapshot boundaries keep
52
- * one order. Recent lines stay in memory for immediate `since` attachment.
51
+ * Numbers every broadcast event and appends it synchronously, so disk, memory and snapshot boundaries
52
+ * keep one order. Recent lines stay in memory to answer an attach with `since` at once.
53
53
  */
54
54
  export class ChatLog {
55
55
  private readonly path: string | null;