@adecore/agents 0.23.1 → 0.24.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 (153) hide show
  1. package/README.md +3 -3
  2. package/dist/chat/attachment-store.js +4 -5
  3. package/dist/chat/backend.d.ts +6 -1
  4. package/dist/chat/background-work.d.ts +1 -1
  5. package/dist/chat/background-work.js +2 -1
  6. package/dist/chat/chat-core.d.ts +53 -161
  7. package/dist/chat/chat-core.js +146 -112
  8. package/dist/chat/chat-log.js +3 -3
  9. package/dist/chat/chat-process.d.ts +21 -0
  10. package/dist/chat/chat-process.js +109 -9
  11. package/dist/chat/chat-session.d.ts +10 -17
  12. package/dist/chat/chat-session.js +141 -182
  13. package/dist/chat/chat-store.d.ts +1 -0
  14. package/dist/chat/chat-store.js +105 -1
  15. package/dist/chat/claude-backend.d.ts +4 -0
  16. package/dist/chat/claude-backend.js +51 -27
  17. package/dist/chat/claude-protocol.d.ts +12 -0
  18. package/dist/chat/claude-protocol.js +137 -7
  19. package/dist/chat/claude-title.d.ts +1 -3
  20. package/dist/chat/claude-title.js +11 -39
  21. package/dist/chat/claude-transcript.d.ts +1 -0
  22. package/dist/chat/claude-transcript.js +36 -17
  23. package/dist/chat/codex-backend.d.ts +21 -1
  24. package/dist/chat/codex-backend.js +168 -10
  25. package/dist/chat/codex-protocol.d.ts +2 -0
  26. package/dist/chat/codex-protocol.js +37 -3
  27. package/dist/chat/codex-thread.js +1 -3
  28. package/dist/chat/codex-transport.d.ts +3 -0
  29. package/dist/chat/codex-transport.js +38 -35
  30. package/dist/chat/context-breakdown.js +2 -2
  31. package/dist/chat/errors.d.ts +1 -1
  32. package/dist/chat/external-sessions.js +2 -2
  33. package/dist/chat/fixtures/claude-2.1.296-goal.json +136 -0
  34. package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10.jsonl +2 -0
  35. package/dist/chat/index.d.ts +29 -0
  36. package/dist/chat/index.js +23 -0
  37. package/dist/chat/projector.d.ts +2 -1
  38. package/dist/chat/projector.js +12 -3
  39. package/dist/chat/request-summary.js +1 -4
  40. package/dist/chat/subagent-settlement.js +1 -3
  41. package/dist/chat/thread.d.ts +3 -0
  42. package/dist/chat/thread.js +20 -3
  43. package/dist/chat/ui-queries.d.ts +2 -0
  44. package/dist/chat/ui-queries.js +40 -5
  45. package/dist/chat/ui-replies.d.ts +33 -0
  46. package/dist/chat/ui-replies.js +152 -0
  47. package/dist/context/index.d.ts +2 -0
  48. package/dist/context/index.js +3 -0
  49. package/dist/coordination/index.d.ts +15 -0
  50. package/dist/coordination/index.js +16 -0
  51. package/dist/host/handlers.js +8 -0
  52. package/dist/host/index.d.ts +7 -0
  53. package/dist/host/index.js +7 -0
  54. package/dist/index.d.ts +9 -0
  55. package/dist/index.js +10 -0
  56. package/dist/providers/accounts/status.js +1 -3
  57. package/dist/providers/claude.js +3 -1
  58. package/dist/providers/codex.js +3 -1
  59. package/dist/providers/index.d.ts +14 -0
  60. package/dist/providers/index.js +12 -0
  61. package/dist/run-process.js +54 -14
  62. package/dist/skills.d.ts +1 -0
  63. package/dist/skills.js +4 -0
  64. package/dist/tasks/task-coordinator.d.ts +4 -0
  65. package/dist/tasks/task-coordinator.js +16 -2
  66. package/dist/tasks/wiring.js +7 -1
  67. package/dist/{providers/accounts/test-accounts.d.ts → testing/accounts.d.ts} +2 -2
  68. package/dist/{providers/accounts/test-accounts.js → testing/accounts.js} +3 -3
  69. package/dist/testing/bin/fake-claude.d.ts +1 -0
  70. package/dist/testing/bin/fake-claude.js +6 -0
  71. package/dist/testing/bin/fake-codex.d.ts +1 -0
  72. package/dist/testing/bin/fake-codex.js +6 -0
  73. package/dist/{chat → testing}/fake-claude.d.ts +1 -1
  74. package/dist/{chat → testing}/fake-claude.js +0 -4
  75. package/dist/{chat → testing}/fake-cli.d.ts +1 -1
  76. package/dist/{chat → testing}/fake-codex.d.ts +1 -1
  77. package/dist/{chat → testing}/fake-codex.js +70 -8
  78. package/dist/testing/index.d.ts +6 -0
  79. package/dist/testing/index.js +7 -0
  80. package/dist/{outbox → testing}/manual-clock.d.ts +1 -1
  81. package/dist/{watch-test-helpers.d.ts → testing/watch.d.ts} +1 -1
  82. package/dist/usage/exchange.d.ts +5 -3
  83. package/dist/usage/exchange.js +25 -16
  84. package/dist/usage/index-file.d.ts +1 -0
  85. package/dist/usage/index-file.js +20 -2
  86. package/dist/usage/index.d.ts +9 -0
  87. package/dist/usage/index.js +6 -0
  88. package/dist/usage/limits/normalize.js +1 -3
  89. package/dist/usage/pricing.d.ts +5 -3
  90. package/dist/usage/pricing.js +28 -19
  91. package/dist/usage/scanner.js +39 -11
  92. package/dist/usage/usage-service.js +8 -3
  93. package/package.json +72 -11
  94. package/src/chat/attachment-store.ts +4 -6
  95. package/src/chat/backend.ts +8 -0
  96. package/src/chat/background-work.ts +2 -2
  97. package/src/chat/chat-core.ts +221 -143
  98. package/src/chat/chat-log.ts +3 -3
  99. package/src/chat/chat-process.ts +128 -8
  100. package/src/chat/chat-session.ts +153 -204
  101. package/src/chat/chat-store.ts +104 -1
  102. package/src/chat/claude-backend.ts +58 -26
  103. package/src/chat/claude-protocol.ts +147 -8
  104. package/src/chat/claude-title.ts +11 -39
  105. package/src/chat/claude-transcript.ts +38 -18
  106. package/src/chat/codex-backend.ts +182 -20
  107. package/src/chat/codex-protocol.ts +40 -5
  108. package/src/chat/codex-thread.ts +1 -4
  109. package/src/chat/codex-transport.ts +45 -35
  110. package/src/chat/context-breakdown.ts +2 -2
  111. package/src/chat/errors.ts +1 -0
  112. package/src/chat/external-sessions.ts +2 -2
  113. package/src/chat/fixtures/claude-2.1.296-goal.json +136 -0
  114. package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10.jsonl +2 -0
  115. package/src/chat/index.ts +49 -0
  116. package/src/chat/projector.ts +13 -4
  117. package/src/chat/request-summary.ts +8 -5
  118. package/src/chat/subagent-settlement.ts +1 -4
  119. package/src/chat/thread.ts +22 -3
  120. package/src/chat/ui-queries.ts +46 -5
  121. package/src/chat/ui-replies.ts +187 -0
  122. package/src/context/index.ts +26 -0
  123. package/src/coordination/index.ts +67 -0
  124. package/src/host/handlers.ts +8 -0
  125. package/src/host/index.ts +8 -0
  126. package/src/index.ts +20 -0
  127. package/src/providers/accounts/status.ts +1 -4
  128. package/src/providers/claude.ts +3 -1
  129. package/src/providers/codex.ts +3 -1
  130. package/src/providers/index.ts +15 -0
  131. package/src/run-process.ts +56 -16
  132. package/src/skills.ts +5 -0
  133. package/src/tasks/task-coordinator.ts +22 -2
  134. package/src/tasks/wiring.ts +7 -1
  135. package/src/{providers/accounts/test-accounts.ts → testing/accounts.ts} +4 -4
  136. package/src/testing/bin/fake-claude.ts +7 -0
  137. package/src/testing/bin/fake-codex.ts +7 -0
  138. package/src/{chat → testing}/fake-claude.ts +1 -5
  139. package/src/{chat → testing}/fake-cli.ts +1 -1
  140. package/src/{chat → testing}/fake-codex.ts +75 -6
  141. package/src/testing/index.ts +7 -0
  142. package/src/{outbox → testing}/manual-clock.ts +1 -1
  143. package/src/{watch-test-helpers.ts → testing/watch.ts} +1 -1
  144. package/src/usage/exchange.ts +31 -20
  145. package/src/usage/index-file.ts +23 -2
  146. package/src/usage/index.ts +10 -0
  147. package/src/usage/limits/normalize.ts +1 -4
  148. package/src/usage/pricing.ts +34 -23
  149. package/src/usage/scanner.ts +48 -11
  150. package/src/usage/usage-service.ts +8 -3
  151. /package/dist/{chat → testing}/fake-cli.js +0 -0
  152. /package/dist/{outbox → testing}/manual-clock.js +0 -0
  153. /package/dist/{watch-test-helpers.js → testing/watch.js} +0 -0
@@ -0,0 +1,67 @@
1
+ // Public API: exports.test.ts snapshots this list, so a name added or dropped here is an API change.
2
+ export { type OutboxEntryOf, OutboxStore, type OutboxWorkShape, type OutboxEntryFields, type OutboxStoreOptions } from '../outbox/outbox.ts';
3
+ export {
4
+ RETRY_DELAYS_MS,
5
+ type OutboxClock,
6
+ systemClock,
7
+ type OutboxOutcome,
8
+ type OutboxHandlers,
9
+ OutboxWorker,
10
+ type OutboxWorkerOptions
11
+ } from '../outbox/outbox-worker.ts';
12
+ export { restartBackgroundLimits, type RestartedOutbox } from '../tasks/background-limit.ts';
13
+ export {
14
+ EndChildrenWorkSchema,
15
+ endChildren,
16
+ type EndChildrenWork,
17
+ type EndChildrenEntry,
18
+ type EndChildrenOutbox,
19
+ type EndChildrenDeps,
20
+ type EndChildren
21
+ } from '../tasks/end-children.ts';
22
+ export { type TaskCoordinatorDeps, TaskCoordinator } from '../tasks/task-coordinator.ts';
23
+ export { TaskStore, type TaskListener, type TaskEvent, type TaskSink } from '../tasks/task-store.ts';
24
+ export {
25
+ BackgroundLimitWorkSchema,
26
+ WakeParentWorkSchema,
27
+ GiveTaskWorkSchema,
28
+ DeliverWaitingWorkSchema,
29
+ type BackgroundLimitWork,
30
+ type WakeParentWork,
31
+ type GiveTaskWork,
32
+ type DeliverWaitingWork,
33
+ type TaskWork,
34
+ type BackgroundLimitEntry,
35
+ type AnyOutboxEntry,
36
+ type TaskOutbox
37
+ } from '../tasks/task-work.ts';
38
+ export {
39
+ type ChatRequests,
40
+ chatRequests,
41
+ WAITING_GRACE_MS,
42
+ WaitingObserver,
43
+ type WaitingRequest,
44
+ type WaitingObserverDeps,
45
+ type WaitingWords
46
+ } from '../tasks/waiting-child.ts';
47
+ export { RESULT_PREVIEW_BYTES, wakePrompt } from '../tasks/wake-parent.ts';
48
+ export { type TaskVerbs, type TaskWords, wireTasks, type TaskChats, type TaskRecord, type TaskWiringDeps, type TaskWiring } from '../tasks/wiring.ts';
49
+ export {
50
+ DeliverMessageWorkSchema,
51
+ deliverMessageHandler,
52
+ type DeliverMessageWork,
53
+ type DeliverMessageEntry,
54
+ type DeliverMessageDeps
55
+ } from '../messages/deliver-message.ts';
56
+ export {
57
+ type MessageWords,
58
+ type ChatNoticeTargets,
59
+ type ChatDelivery,
60
+ chatNoticeTargets,
61
+ deliverToChat,
62
+ showNotices,
63
+ type NoticeChat
64
+ } from '../messages/deliver-notice.ts';
65
+ export { NoticeNotes, unshownNotes } from '../messages/notice-notes.ts';
66
+ export { MAX_NOTICES, NOTICE_MAX_AGE_MS, type Notice, NoticeStore } from '../messages/notice-store.ts';
67
+ export { AgentLineageStore } from '../lineage.ts';
@@ -55,6 +55,10 @@ export function chatHandlers(chats: ChatCore, providers: ProviderRegistry): { [T
55
55
  await chats.stopTask(payload.chatId, payload.taskId);
56
56
  return {};
57
57
  },
58
+ 'chat.goal': async (payload) => {
59
+ await chats.goal(payload.chatId, payload.action);
60
+ return {};
61
+ },
58
62
  'chat.detach': (payload, clientId) => {
59
63
  chats.detach(payload.chatId, clientId);
60
64
  return {};
@@ -112,6 +116,10 @@ export function chatHandlers(chats: ChatCore, providers: ProviderRegistry): { [T
112
116
  await chats.kill(payload.chatId);
113
117
  return {};
114
118
  },
119
+ 'chat.restart': async (payload) => {
120
+ await chats.restart(payload.chatId);
121
+ return {};
122
+ },
115
123
  'chat.list': () => ({ chats: chats.list() })
116
124
  };
117
125
  }
@@ -0,0 +1,8 @@
1
+ // Public API: exports.test.ts snapshots this list, so a name added or dropped here is an API change.
2
+ export { AgentHost, type AgentHostOptions } from './agent-host.ts';
3
+ export { RUIMTE_SESSION_VARIABLES, cliEnvironment, type CliEnvironmentPolicy } from './environment.ts';
4
+ export { type AgentHandler, type AgentHandlers, chatHandlers, accountHandlers, usageHandlers, type AgentRequestMap, type ChatRequestType } from './handlers.ts';
5
+ export { memoryPortPair } from './memory-port.ts';
6
+ export { type AgentWiring, wireAgents, type AgentWiringOptions } from './wiring.ts';
7
+ export { ClientSinks } from '../client-sinks.ts';
8
+ export type { AgentEvent, AgentEventMap, AgentSink } from '../events.ts';
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ // Public API: exports.test.ts snapshots this list, so a name added or dropped here is an API change.
2
+ export { wait, withTimeout } from './async.ts';
3
+ export { CodedError } from './coded-error.ts';
4
+ export { describeError, setErrorStacks, errorText } from './error-text.ts';
5
+ export { tempNameFor, writeAtomic, replaceSync, writeAtomicSync, isNotFound, fileExists, type WriteAtomicOptions } from './fs.ts';
6
+ export { recordFileName, RecordDirectory, type RecordDirectoryOptions } from './record-directory.ts';
7
+ export { runProcess, type ProcessResult, type RunProcessOptions } from './run-process.ts';
8
+ export { Serializer, KeyedSerializer } from './serializer.ts';
9
+ export { cleanTitle, readLines } from './title-file.ts';
10
+ export {
11
+ type DirectoryWatcher,
12
+ type WatchSeams,
13
+ supportsRecursive,
14
+ SYSTEM_WATCH,
15
+ type Settled,
16
+ settled,
17
+ PerClientWatches,
18
+ type WatchDirectory,
19
+ type Schedule
20
+ } from './watch-seam.ts';
@@ -1,4 +1,5 @@
1
1
  import type { AgentKind } from '@adecore/agent-contracts';
2
+ import { isRecord } from '@adecore/agent-contracts';
2
3
  import { withTimeout } from '../../async.ts';
3
4
  import { CodexTransport, type CodexClientInfo } from '../../chat/codex-transport.ts';
4
5
  import { runProcess } from '../../run-process.ts';
@@ -20,10 +21,6 @@ function text(value: unknown): string | null {
20
21
  return typeof value === 'string' && value !== '' ? value : null;
21
22
  }
22
23
 
23
- function isRecord(value: unknown): value is Record<string, unknown> {
24
- return typeof value === 'object' && value !== null && !Array.isArray(value);
25
- }
26
-
27
24
  /* `claude auth status --json`, as Claude Code 2.1.282 prints it: `loggedIn`, `email`, `orgName`, `subscriptionType`. */
28
25
  export function readClaudeAuthStatus(output: string): AccountReading {
29
26
  let status: unknown;
@@ -101,6 +101,7 @@ export const CLAUDE_CAPABILITIES: ProviderCapabilities = {
101
101
  denyReason: true,
102
102
  allowAlways: true,
103
103
  asyncQuestions: false,
104
+ steer: true,
104
105
  compaction: 'prompt',
105
106
  reportsCost: true,
106
107
  /*
@@ -110,7 +111,8 @@ export const CLAUDE_CAPABILITIES: ProviderCapabilities = {
110
111
  */
111
112
  reportsContextWindow: false,
112
113
  reportsThinking: true,
113
- slashCommands: true
114
+ slashCommands: true,
115
+ goals: 'command'
114
116
  };
115
117
 
116
118
  // Claude Code takes its flags before `--resume`, which takes the id.
@@ -58,11 +58,13 @@ export const CODEX_CAPABILITIES: ProviderCapabilities = {
58
58
  denyReason: false,
59
59
  allowAlways: true,
60
60
  asyncQuestions: true,
61
+ steer: true,
61
62
  compaction: 'native',
62
63
  reportsCost: false,
63
64
  reportsContextWindow: true,
64
65
  reportsThinking: true,
65
- slashCommands: false
66
+ slashCommands: false,
67
+ goals: 'native'
66
68
  };
67
69
 
68
70
  // `codex resume` is a subcommand that reads `[OPTIONS] [SESSION_ID]`, so the flags go after it.
@@ -0,0 +1,15 @@
1
+ // Public API: exports.test.ts snapshots this list, so a name added or dropped here is an API change.
2
+ export { AccountError, type AccountLaunches, storedAccount, launchEnv } from './accounts/launch.ts';
3
+ export { ProviderAccountsService, type ProviderAccountsOptions } from './accounts/service.ts';
4
+ export type { ShadowHomeReport } from './accounts/shadow-home.ts';
5
+ export type { AccountReading, AskAccount } from './accounts/status.ts';
6
+ export { type AccountsHost, variablesProblem, keychainService, type SecretStore, type ReservedVariables } from './accounts/variables.ts';
7
+ export { ModelCatalog, type ModelCatalogInput } from './catalog.ts';
8
+ export { createClaudeProvider } from './claude-provider.ts';
9
+ export { createCodexProvider } from './codex-provider.ts';
10
+ export { type CodexRules, codexRulesText, codexRulesPathIn, defaultCodexHome, installCodexRules, type CodexRulesResult } from './codex-rules.ts';
11
+ export { codexThreadOptions, codexServiceTier } from './codex.ts';
12
+ export { type CliDetection, detectCli } from './detect.ts';
13
+ export type { ChatProvider, ProviderHome } from './provider.ts';
14
+ export { ProviderRegistry, type ProviderRegistryOptions } from './registry.ts';
15
+ export { MODE_ORDER, narrowerMode, ceilingForOpening, modeFlag } from '../modes.ts';
@@ -1,4 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { killProcessTree } from './chat/chat-process.ts';
2
3
 
3
4
  export interface ProcessResult {
4
5
  // Null when the process ended on a signal.
@@ -12,11 +13,16 @@ export interface RunProcessOptions {
12
13
  cwd?: string;
13
14
  // Written and closed at once; absent, the process reads from nothing.
14
15
  stdin?: string;
15
- // The process is sent a SIGTERM after this, and the run fails.
16
+ // The process and what it started are sent a SIGTERM after this, a SIGKILL after a grace, and the run fails.
16
17
  timeoutMs?: number;
17
18
  timeoutMessage?: string;
18
19
  }
19
20
 
21
+ // What a command and everything it started get between the SIGTERM and the SIGKILL.
22
+ const KILL_GRACE_MS = 2000;
23
+ // Stdout and stderr together; a short command that prints more is ended and fails instead of filling memory.
24
+ const MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
25
+
20
26
  /* One short command to its end, with what it printed. A command that cannot start rejects. */
21
27
  export function runProcess(command: readonly string[], options: RunProcessOptions = {}): Promise<ProcessResult> {
22
28
  return new Promise((resolve, reject) => {
@@ -28,30 +34,64 @@ export function runProcess(command: readonly string[], options: RunProcessOption
28
34
  const child = spawn(executable, args, {
29
35
  ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
30
36
  ...(options.env === undefined ? {} : { env: options.env }),
31
- stdio: [options.stdin === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe']
37
+ stdio: [options.stdin === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'],
38
+ // A group of its own, so ending the command also ends what it started; Windows ends the tree through `taskkill`.
39
+ detached: process.platform !== 'win32',
40
+ windowsHide: true
32
41
  });
33
42
  const stdout: Buffer[] = [];
34
43
  const stderr: Buffer[] = [];
35
- child.stdout?.on('data', (chunk: Buffer) => stdout.push(chunk));
36
- child.stderr?.on('data', (chunk: Buffer) => stderr.push(chunk));
37
- const timer =
38
- options.timeoutMs === undefined
39
- ? null
40
- : setTimeout(() => {
41
- child.kill('SIGTERM');
42
- reject(new Error(options.timeoutMessage ?? `${executable} did not finish in time`));
43
- }, options.timeoutMs);
44
- child.on('error', (error) => {
44
+ let printed = 0;
45
+ let settled = false;
46
+ let timer: ReturnType<typeof setTimeout> | null = null;
47
+ const settle = (): boolean => {
48
+ if (settled) {
49
+ return false;
50
+ }
51
+ settled = true;
45
52
  if (timer !== null) {
46
53
  clearTimeout(timer);
47
54
  }
48
- reject(error);
55
+ return true;
56
+ };
57
+ const end = (message: string): void => {
58
+ if (!settle()) {
59
+ return;
60
+ }
61
+ const pid = child.pid;
62
+ if (pid !== undefined) {
63
+ killProcessTree(pid, 'SIGTERM');
64
+ // Sent even when the command left in time: what it started may still ignore the SIGTERM.
65
+ setTimeout(() => killProcessTree(pid, 'SIGKILL'), KILL_GRACE_MS).unref?.();
66
+ }
67
+ reject(new Error(message));
68
+ };
69
+ const collect = (into: Buffer[]) => (chunk: Buffer) => {
70
+ if (settled) {
71
+ return;
72
+ }
73
+ printed += chunk.length;
74
+ if (printed > MAX_OUTPUT_BYTES) {
75
+ end(`${executable} printed more than ${MAX_OUTPUT_BYTES / 1024 / 1024} MB`);
76
+ return;
77
+ }
78
+ into.push(chunk);
79
+ };
80
+ child.stdout?.on('data', collect(stdout));
81
+ child.stderr?.on('data', collect(stderr));
82
+ if (options.timeoutMs !== undefined) {
83
+ const message = options.timeoutMessage ?? `${executable} did not finish in time`;
84
+ timer = setTimeout(() => end(message), options.timeoutMs);
85
+ }
86
+ child.on('error', (error) => {
87
+ if (settle()) {
88
+ reject(error);
89
+ }
49
90
  });
50
91
  child.on('close', (exitCode) => {
51
- if (timer !== null) {
52
- clearTimeout(timer);
92
+ if (settle()) {
93
+ resolve({ exitCode, stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8') });
53
94
  }
54
- resolve({ exitCode, stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8') });
55
95
  });
56
96
  if (options.stdin !== undefined && child.stdin) {
57
97
  child.stdin.on('error', () => undefined);
package/src/skills.ts CHANGED
@@ -195,4 +195,9 @@ export class SkillIndex {
195
195
  this.cache.set(key, { at: Date.now(), skills });
196
196
  return skills;
197
197
  }
198
+
199
+ /* Drops the scan of one CLI in one folder, so the next look reads the disk again. */
200
+ forget(kind: AgentKind, cwd: string): void {
201
+ this.cache.delete(`${kind}:${cwd}`);
202
+ }
198
203
  }
@@ -27,6 +27,11 @@ export interface TaskCoordinatorDeps {
27
27
  * it (`reports`), nothing (`silent`), or nothing ever again, since its process went (`gone`).
28
28
  */
29
29
  afterBackgroundWork(chatId: string): 'reports' | 'silent' | 'gone';
30
+ /*
31
+ * The goal a chat's CLI works toward, while one is active: `running` while its process is there to open the
32
+ * next turn toward it; null once the goal is met, paused or cleared, or when there never was one.
33
+ */
34
+ activeGoal?(chatId: string): { objective: string; running: boolean } | null;
30
35
  /* The commands and monitors a chat's CLI runs in the background right now. */
31
36
  commandsOf(chatId: string): readonly ChatBackgroundTask[];
32
37
  /* The `background-limit` outbox entry of a task, which settles it once its commands ran too long. */
@@ -73,6 +78,10 @@ function restartedNote(commands: readonly string[]): string {
73
78
  return `The machine restarted while this still ran in the background: ${commandList(commands)}.`;
74
79
  }
75
80
 
81
+ function goalLostNote(objective: string): string {
82
+ return `The agent stopped before its goal was met: ${objective}`;
83
+ }
84
+
76
85
  /* What a settled turn of a child makes of its task. */
77
86
  export function resultOfTurn(turn: ChatTurnItem, items: readonly ChatItem[], at: number): { status: 'done' | 'failed'; result: TaskResult } {
78
87
  if (turn.state === 'done') {
@@ -90,8 +99,9 @@ export function resultOfTurn(turn: ChatTurnItem, items: readonly ChatItem[], at:
90
99
  /*
91
100
  * Settles tasks from what children do and does nothing else: it writes the task store and owes wakes
92
101
  * in the outbox, never starts a turn, since it runs inside the broadcast of the child it heard. A chat
93
- * child settles when a turn ends with none of its own tasks still open or waiting to wake it and no work
94
- * of its CLI's still running in the background, or once the commands it runs there outlast their limit;
102
+ * child settles when a turn ends with none of its own tasks still open or waiting to wake it, no goal
103
+ * still active and no work of its CLI's still running in the background, or once the commands it runs
104
+ * there outlast their limit;
95
105
  * an agent that is not a chat, which the host tells it about, settles only on `done`, or fails when it
96
106
  * ends without one.
97
107
  */
@@ -236,6 +246,16 @@ export class TaskCoordinator {
236
246
  this.hold(task, paused);
237
247
  return;
238
248
  }
249
+ // A goal holds the task with no limit, and the parent sleeps on through the turns its CLI opens toward it.
250
+ const goal = turn.state === 'done' ? (this.deps.activeGoal?.(chatId) ?? null) : null;
251
+ if (goal?.running === true) {
252
+ return;
253
+ }
254
+ if (goal !== null) {
255
+ const { result } = resultOfTurn(turn, items, this.deps.now());
256
+ this.settle(task, 'failed', { ...result, text: `${result.text}\n\n${goalLostNote(goal.objective)}`, source: 'exit' });
257
+ return;
258
+ }
239
259
  // A turn a person stopped or that failed ends the task as it always did, whatever still runs beside it.
240
260
  if (turn.state === 'done' && this.heldInBackground(chatId, task, items)) {
241
261
  this.track(
@@ -106,6 +106,11 @@ export function wireTasks(deps: TaskWiringDeps): TaskWiring {
106
106
  return reportsOnBackgroundWork(chat.info.provider) ? 'reports' : 'silent';
107
107
  },
108
108
  commandsOf: (chatId) => chats.get(chatId)?.info.background ?? [],
109
+ activeGoal: (chatId) => {
110
+ const chat = chats.get(chatId);
111
+ const goal = chat?.info.goal;
112
+ return chat && goal?.status === 'active' ? { objective: goal.objective, running: chat.running } : null;
113
+ },
109
114
  limit
110
115
  });
111
116
 
@@ -142,7 +147,8 @@ export function wireTasks(deps: TaskWiringDeps): TaskWiring {
142
147
  }
143
148
  const stored = await chats.storedBackground(task.childId);
144
149
  const storedWork = stored.items.length > 0 || stored.commands.length > 0;
145
- if (!deps.placed(task.childId) || (task.background === undefined && task.requiresTaskTurn !== true && !storedWork)) {
150
+ // A goal still active went down with the CLI, which its child's last turn settles on.
151
+ if (!deps.placed(task.childId) || (task.background === undefined && task.requiresTaskTurn !== true && !storedWork && !stored.goal)) {
146
152
  return;
147
153
  }
148
154
  if (task.background === undefined && stored.turnId !== null && storedWork) {
@@ -1,8 +1,8 @@
1
1
  import type { AgentKind, ProviderAccountMap, ProviderInfo } from '@adecore/agent-contracts';
2
- import { providerFor } from '../registry.ts';
3
- import { ProviderAccountsService } from './service.ts';
4
- import { accountsPath, writeAccounts } from './store.ts';
5
- import type { SecretStore } from './variables.ts';
2
+ import { providerFor } from '../providers/registry.ts';
3
+ import { ProviderAccountsService } from '../providers/accounts/service.ts';
4
+ import { accountsPath, writeAccounts } from '../providers/accounts/store.ts';
5
+ import type { SecretStore } from '../providers/accounts/variables.ts';
6
6
 
7
7
  /* A keychain in memory; `values` is what it holds, by key. */
8
8
  export function memorySecrets(values = new Map<string, string>()): SecretStore & { values: Map<string, string> } {
@@ -0,0 +1,7 @@
1
+ import { fakeClaude } from '../fake-claude.ts';
2
+ import { runOverStdio } from '../fake-cli.ts';
3
+
4
+ // Run as a script, it stands in for the Claude CLI behind real pipes; imported, it does nothing.
5
+ if (import.meta.main) {
6
+ await runOverStdio(fakeClaude);
7
+ }
@@ -0,0 +1,7 @@
1
+ import { fakeCodex } from '../fake-codex.ts';
2
+ import { runOverStdio } from '../fake-cli.ts';
3
+
4
+ // Run as a script, it stands in for `codex app-server` behind real pipes; imported, it does nothing.
5
+ if (import.meta.main) {
6
+ await runOverStdio(fakeCodex);
7
+ }
@@ -5,7 +5,7 @@
5
5
  import { spawn } from 'node:child_process';
6
6
  import { writeFileSync } from 'node:fs';
7
7
  import { resolve } from 'node:path';
8
- import { runOverStdio, type FakeCli, type FakeIo, type FakeProgram } from './fake-cli.ts';
8
+ import type { FakeCli, FakeIo, FakeProgram } from './fake-cli.ts';
9
9
 
10
10
  export function fakeClaude(io: FakeIo): FakeProgram {
11
11
  const args = io.argv;
@@ -1074,7 +1074,3 @@ export function claudeStoppingOnResume(sessionIds: ReadonlySet<string>, how: 'mi
1074
1074
  return { onLine: () => undefined };
1075
1075
  };
1076
1076
  }
1077
-
1078
- if (import.meta.main) {
1079
- await runOverStdio(fakeClaude);
1080
- }
@@ -1,4 +1,4 @@
1
- import type { ChatProcess, ChatSpawnOptions, SpawnChatProcess } from './chat-process.ts';
1
+ import type { ChatProcess, ChatSpawnOptions, SpawnChatProcess } from '../chat/chat-process.ts';
2
2
 
3
3
  /* What a fake CLI has instead of `process`, so the same fake runs in a test and as a child process. */
4
4
  export interface FakeIo {
@@ -1,8 +1,8 @@
1
1
  /*
2
2
  * Network-free `codex app-server` fake. Prompt commands exercise approvals, edits, questions,
3
- * interrupts, failures, names, subagents, pagination and forks through the real JSON-RPC shapes.
3
+ * interrupts, failures, names, subagents, pagination, forks and goals through the real JSON-RPC shapes.
4
4
  */
5
- import { runOverStdio, type FakeCli } from './fake-cli.ts';
5
+ import type { FakeCli } from './fake-cli.ts';
6
6
 
7
7
  type Frame = Record<string, unknown>;
8
8
 
@@ -119,6 +119,37 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
119
119
  turnId = '';
120
120
  };
121
121
 
122
+ /*
123
+ * The thread's goal (Codex 0.162.1). An objective `steps:N` takes N turns: the first is the one the client
124
+ * starts, which ends when the test says later is, and Codex opens every next one itself while the goal
125
+ * is active, also later, and marks the goal complete in the last.
126
+ */
127
+ let goal: Frame | null = null;
128
+ let goalSteps = 0;
129
+
130
+ const goalChanged = (): void => {
131
+ notify('thread/goal/updated', { threadId, turnId: turnId === '' ? null : turnId, goal });
132
+ };
133
+
134
+ const goalStep = (): void => {
135
+ goalSteps += 1;
136
+ agentMessage(`goal step ${goalSteps}`);
137
+ const needed = Number(/steps:(\d+)/.exec(String(goal?.objective ?? ''))?.[1] ?? '1');
138
+ if (goal !== null) {
139
+ goal = { ...goal, tokensUsed: Number(goal.tokensUsed) + 100, status: goalSteps >= needed ? 'complete' : goal.status };
140
+ goalChanged();
141
+ }
142
+ turnCompleted('completed');
143
+ if (goal?.status === 'active') {
144
+ io.later(() => {
145
+ if (goal?.status === 'active') {
146
+ turnStarted();
147
+ goalStep();
148
+ }
149
+ });
150
+ }
151
+ };
152
+
122
153
  let pendingApproval: PendingApproval | null = null;
123
154
  let pendingQuestion: { rpcId: number; questionId: string } | null = null;
124
155
  let pendingSteer = false;
@@ -160,6 +191,10 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
160
191
  const user = item('userMessage', { clientId: null, content: [{ type: 'text', text, text_elements: [] }] });
161
192
  started(user);
162
193
  completed(user);
194
+ if (goal !== null && (text === goal.objective || text === '')) {
195
+ io.later(goalStep);
196
+ return;
197
+ }
163
198
  if (text === 'crash') {
164
199
  io.exit(1);
165
200
  return;
@@ -525,6 +560,44 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
525
560
  turnCompleted('interrupted');
526
561
  }
527
562
  return;
563
+ case 'thread/goal/get':
564
+ out({ id, result: { goal } });
565
+ return;
566
+ case 'thread/goal/set': {
567
+ const at = Math.floor(Date.now() / 1000);
568
+ if (typeof params.objective === 'string') {
569
+ goal = {
570
+ threadId,
571
+ objective: params.objective,
572
+ status: 'active',
573
+ tokenBudget: null,
574
+ tokensUsed: 0,
575
+ timeUsedSeconds: 0,
576
+ createdAt: at,
577
+ updatedAt: at
578
+ };
579
+ goalSteps = 0;
580
+ }
581
+ if (goal === null) {
582
+ out({ id, error: { code: -32600, message: 'no goal to update' } });
583
+ return;
584
+ }
585
+ if (typeof params.status === 'string') {
586
+ goal = { ...goal, status: params.status, updatedAt: at };
587
+ }
588
+ out({ id, result: { goal } });
589
+ goalChanged();
590
+ return;
591
+ }
592
+ case 'thread/goal/clear': {
593
+ const cleared = goal !== null;
594
+ goal = null;
595
+ out({ id, result: { cleared } });
596
+ if (cleared) {
597
+ notify('thread/goal/cleared', { threadId });
598
+ }
599
+ return;
600
+ }
528
601
  case 'thread/compact/start':
529
602
  out({ id, result: {} });
530
603
  turnStarted();
@@ -562,7 +635,3 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
562
635
  }
563
636
 
564
637
  export const fakeCodex: FakeCli = fakeCodexWith({});
565
-
566
- if (import.meta.main) {
567
- await runOverStdio(fakeCodex);
568
- }
@@ -0,0 +1,7 @@
1
+ // Public API: exports.test.ts snapshots this list, so a name added or dropped here is an API change.
2
+ export { type FakeIo, type FakeProgram, type FakeCli, type StartedFake, type InProcessCli, inProcess, runOverStdio } from './fake-cli.ts';
3
+ export { fakeClaude, claudeStoppingOnResume } from './fake-claude.ts';
4
+ export { FAKE_CHILD_STEPS, fakeCodexForks, type FakeCodexOptions, fakeCodexWith, fakeCodex } from './fake-codex.ts';
5
+ export { memorySecrets, testAccounts } from './accounts.ts';
6
+ export { ManualClock } from './manual-clock.ts';
7
+ export { FakeDirectoryWatcher, FakeWatch, EventLog } from './watch.ts';
@@ -1,4 +1,4 @@
1
- import type { OutboxClock } from './outbox-worker.ts';
1
+ import type { OutboxClock } from '../outbox/outbox-worker.ts';
2
2
 
3
3
  /* A clock a test moves by hand. A timer fires only inside `advance`, never on its own. */
4
4
  export class ManualClock implements OutboxClock {
@@ -1,4 +1,4 @@
1
- import type { DirectoryWatcher, WatchDirectory, WatchSeams } from './watch-seam.ts';
1
+ import type { DirectoryWatcher, WatchDirectory, WatchSeams } from '../watch-seam.ts';
2
2
 
3
3
  export class FakeDirectoryWatcher implements DirectoryWatcher {
4
4
  readonly path: string;