@agentshouse/kit 0.2.1-cbabe8ad337ff → 0.2.1-cc719fffeec71

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 (96) hide show
  1. package/README.md +30 -29
  2. package/dist/acp.js +34 -14
  3. package/dist/agents.js +109 -22
  4. package/dist/api.js +6 -2
  5. package/dist/app-git.js +10 -26
  6. package/dist/bootstrap.json +4 -4
  7. package/dist/bridge.js +10 -24
  8. package/dist/chatgpt.js +32 -0
  9. package/dist/clis.js +51 -9
  10. package/dist/conversations.js +140 -70
  11. package/dist/copies.js +195 -119
  12. package/dist/end.js +18 -3
  13. package/dist/enrol-main.js +7 -1
  14. package/dist/events.js +4 -0
  15. package/dist/files.js +8 -2
  16. package/dist/git.js +10 -8
  17. package/dist/home.js +96 -7
  18. package/dist/house-cli.js +291 -0
  19. package/dist/house-main.js +7 -177
  20. package/dist/inputs.js +37 -0
  21. package/dist/instructions.js +10 -32
  22. package/dist/kit-cli.js +68 -0
  23. package/dist/kit-main.js +8 -22
  24. package/dist/login.js +112 -22
  25. package/dist/logout.js +0 -2
  26. package/dist/parts.js +34 -16
  27. package/dist/placement.js +30 -0
  28. package/dist/relay.js +12 -2
  29. package/dist/resident.js +82 -26
  30. package/dist/service-main.js +9 -0
  31. package/dist/service.js +55 -0
  32. package/dist/sign-in.js +5 -23
  33. package/dist/skills.js +81 -12
  34. package/dist/stream.js +68 -17
  35. package/dist/transcription.js +3 -3
  36. package/dist/update.js +48 -29
  37. package/dist/wire.js +6 -0
  38. package/dist/work.js +3 -5
  39. package/how-we-work.md +13 -3
  40. package/package.json +3 -2
  41. package/skills/ask-matt/SKILL.md +17 -11
  42. package/skills/ask-matt/agents/openai.yaml +1 -0
  43. package/skills/code-review/SKILL.md +5 -5
  44. package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
  45. package/skills/codebase-design/SKILL.md +0 -2
  46. package/skills/diagnosing-bugs/SKILL.md +2 -4
  47. package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
  48. package/skills/domain-modeling/SKILL.md +12 -14
  49. package/skills/grill-me/SKILL.md +1 -2
  50. package/skills/grill-me/agents/openai.yaml +1 -0
  51. package/skills/grill-with-docs/SKILL.md +1 -2
  52. package/skills/grill-with-docs/agents/openai.yaml +1 -0
  53. package/skills/grilling/SKILL.md +2 -2
  54. package/skills/handoff/SKILL.md +2 -3
  55. package/skills/handoff/agents/openai.yaml +1 -0
  56. package/skills/implement/SKILL.md +5 -4
  57. package/skills/implement/agents/openai.yaml +1 -0
  58. package/skills/implement-spec/SKILL.md +40 -0
  59. package/skills/implement-spec/agents/openai.yaml +5 -0
  60. package/skills/improve-codebase-architecture/SKILL.md +6 -7
  61. package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
  62. package/skills/pr/CREDITS.md +3 -0
  63. package/skills/pr/LICENSE +21 -0
  64. package/skills/pr/SKILL.md +170 -0
  65. package/skills/pr/agents/openai.yaml +3 -0
  66. package/skills/prototype/SKILL.md +0 -2
  67. package/skills/research/SKILL.md +0 -2
  68. package/skills/retro/LICENSE +21 -0
  69. package/skills/retro/SKILL.md +44 -0
  70. package/skills/retro/agents/openai.yaml +5 -0
  71. package/skills/tdd/SKILL.md +2 -4
  72. package/skills/teach/SKILL.md +3 -4
  73. package/skills/teach/agents/openai.yaml +1 -0
  74. package/skills/to-questionnaire/SKILL.md +1 -2
  75. package/skills/to-questionnaire/agents/openai.yaml +1 -0
  76. package/skills/to-spec/SKILL.md +1 -2
  77. package/skills/to-spec/agents/openai.yaml +1 -0
  78. package/skills/to-tickets/SKILL.md +3 -4
  79. package/skills/to-tickets/agents/openai.yaml +1 -0
  80. package/skills/triage/AGENT-BRIEF.md +207 -0
  81. package/skills/triage/LICENSE +21 -0
  82. package/skills/triage/OUT-OF-SCOPE.md +105 -0
  83. package/skills/triage/SKILL.md +112 -0
  84. package/skills/triage/agents/openai.yaml +5 -0
  85. package/skills/wait-what/SKILL.md +2 -3
  86. package/skills/wait-what/agents/openai.yaml +1 -0
  87. package/skills/wayfinder/SKILL.md +5 -6
  88. package/skills/wayfinder/agents/openai.yaml +1 -0
  89. package/skills/wizard/LICENSE +21 -0
  90. package/skills/wizard/SKILL.md +44 -0
  91. package/skills/wizard/agents/openai.yaml +3 -0
  92. package/skills/wizard/template.sh +223 -0
  93. package/skills/writing-for-agents/SKILL.md +0 -2
  94. package/skills/resolving-merge-conflicts/SKILL.md +0 -16
  95. package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
  96. /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
package/dist/bridge.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { once } from 'node:events';
3
3
  import { setTimeout as delay } from 'node:timers/promises';
4
- import { mkdir } from 'node:fs/promises';
5
4
  import { createServer, request as httpRequest } from 'node:http';
6
5
  import { request as httpsRequest } from 'node:https';
7
6
  import { delimiter, dirname, join } from 'node:path';
@@ -9,7 +8,7 @@ import { parse } from 'yaml';
9
8
  import { finalRefusal, HouseRefusal, retryDelay, retrying } from './api.js';
10
9
  import { loginPath } from './clis.js';
11
10
  import { changedPaths, } from './copies.js';
12
- import { kitHome, readEnrolment } from './home.js';
11
+ import { kitHome, readEnrolment, socketDirectory } from './home.js';
13
12
  import { callHouse, mcpBody, rpc, UNREACHABLE } from './mcp.js';
14
13
  import { operationId } from './operation.js';
15
14
  import { refusalOf, retryable } from './refusals.js';
@@ -82,7 +81,7 @@ function editAnswer(forwarded) {
82
81
  export async function openBridge(house, copies, conversationId, signal) {
83
82
  const path = [join(kitHome(), 'shim'), dirname(process.execPath), await loginPath()].join(delimiter);
84
83
  const origin = (await readEnrolment()).house;
85
- const { credential } = await house.post(`/kit/conversations/${conversationId}/credential`, {}, signal);
84
+ const { credential } = await retrying(() => house.post(`/kit/conversations/${conversationId}/credential`, {}, signal), signal);
86
85
  const closed = new AbortController();
87
86
  const sent = (message, operation) => callHouse(origin, credential, message, operation, closed.signal);
88
87
  const send = async (message, operation) => {
@@ -97,7 +96,7 @@ export async function openBridge(house, copies, conversationId, signal) {
97
96
  const after = resendAfter(forwarded, attempt);
98
97
  if (after === null)
99
98
  return forwarded;
100
- // A write House did not settle is sent again on the jittered curve, or after the wait House names for a pending one.
99
+ // A Tool call House did not settle is sent again on the jittered curve, or after the wait House names for a pending one.
101
100
  await delay(after, undefined, { signal: closed.signal });
102
101
  }
103
102
  };
@@ -135,23 +134,9 @@ export async function openBridge(house, copies, conversationId, signal) {
135
134
  const held = JSON.parse(text).transfer;
136
135
  return { url: held.url, operation: held.operation, bytes: bytes.length, sha256 };
137
136
  };
138
- let mutations = null;
139
- const writes = async (tool) => {
140
- mutations ??= house
141
- .post('/kit/tools/mutations', {}, closed.signal)
142
- .then(({ tools }) => new Set(tools.filter((listed) => listed.writes).map((listed) => listed.name)));
143
- try {
144
- return (await mutations).has(tool);
145
- }
146
- catch (error) {
147
- mutations = null;
148
- throw error;
149
- }
150
- };
151
- const forward = async (text) => {
137
+ const forward = (text) => {
152
138
  const message = JSON.parse(text);
153
- const tool = message.method === 'tools/call' ? String(message.params.name) : null;
154
- return send(message, tool !== null && (await writes(tool)) ? operationId() : null);
139
+ return send(message, message.method === 'tools/call' ? operationId() : null);
155
140
  };
156
141
  const caller = {
157
142
  key: `conversation:${conversationId}`,
@@ -173,15 +158,15 @@ export async function openBridge(house, copies, conversationId, signal) {
173
158
  const { cwd, commit } = JSON.parse(text);
174
159
  return { status: 200, text: JSON.stringify(await copies.push(cwd, commit, caller)) };
175
160
  }
161
+ if (path === '/git/fetch')
162
+ return { status: 200, text: JSON.stringify(await copies.fetch()) };
176
163
  if (path === '/git/observed') {
177
164
  await copies.observed(JSON.parse(text));
178
165
  return { status: 200, text: '{}' };
179
166
  }
180
167
  return path === '/' ? forward(text) : { status: 404, text: '' };
181
168
  };
182
- const sockets = join(kitHome(), 'bridges');
183
- await mkdir(sockets, { recursive: true, mode: 0o700 });
184
- const socketPath = join(sockets, `${randomUUID()}.sock`);
169
+ const socketPath = join(socketDirectory(), `${randomUUID()}.sock`);
185
170
  const bridge = createServer(async (request, response) => {
186
171
  try {
187
172
  const path = request.url ?? '';
@@ -197,7 +182,8 @@ export async function openBridge(house, copies, conversationId, signal) {
197
182
  response.writeHead(error.status, { 'content-type': 'application/json' }).end(error.text);
198
183
  return;
199
184
  }
200
- response.writeHead(502).end(request.url === '/git/push' && error instanceof Error ? error.message : UNREACHABLE);
185
+ const verb = request.url === '/git/push' || request.url === '/git/fetch';
186
+ response.writeHead(502).end(verb && error instanceof Error ? error.message : UNREACHABLE);
201
187
  }
202
188
  });
203
189
  bridge.listen(socketPath);
@@ -0,0 +1,32 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { placeAppGit } from './app-git.js';
3
+ import { kitHome, readOwnAgent, writeOwnAgent } from './home.js';
4
+ import { accepted, DEFAULT_HOUSE } from './login.js';
5
+ const KEY_SOURCE = 'New key for a script on Connections';
6
+ const USAGE = `usage: kit chatgpt --key <key> [--house <origin>]
7
+ --key <key> a key from ${KEY_SOURCE}
8
+ --house <origin> the House of the key, ${DEFAULT_HOUSE} unless one is stored
9
+ `;
10
+ export async function chatgpt(argv) {
11
+ const { values } = parseArgs({
12
+ args: argv,
13
+ options: { key: { type: 'string' }, house: { type: 'string' }, help: { type: 'boolean', default: false } },
14
+ });
15
+ if (values.help) {
16
+ process.stdout.write(USAGE);
17
+ return;
18
+ }
19
+ if (values.key === undefined)
20
+ throw new Error(`pass --key with a key from ${KEY_SOURCE}`);
21
+ const before = await readOwnAgent();
22
+ const house = values.house ?? before?.house ?? DEFAULT_HOUSE;
23
+ if (!(await accepted(house, values.key)))
24
+ throw new Error(`${house} does not accept this key; pass one from ${KEY_SOURCE}`);
25
+ const relay = process.env.HOUSE_KIT_RELAY || undefined;
26
+ await writeOwnAgent({ house, ...(relay === undefined ? {} : { relay }), credential: values.key });
27
+ await placeAppGit(await readOwnAgent()).catch((error) => process.stderr.write(`kit: ${error.message}\n`));
28
+ if (before !== null && before.credential !== values.key) {
29
+ process.stdout.write('This replaced the key this Kit home held; give each session its own HOUSE_KIT_HOME to keep its own key.\n');
30
+ }
31
+ process.stdout.write(`house now calls ${house} as you, with the key kept in ${kitHome()}.\n`);
32
+ }
package/dist/clis.js CHANGED
@@ -1,9 +1,14 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { constants, readFileSync } from 'node:fs';
3
- import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import { access, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
4
5
  import { homedir, userInfo } from 'node:os';
5
- import { delimiter, isAbsolute, join } from 'node:path';
6
+ import { delimiter, dirname, isAbsolute, join } from 'node:path';
6
7
  import { kitHome } from './home.js';
8
+ const HOUSE_EXECUTABLES = /^host_executable\(name = "house", paths = (\[.*\])\)$/m;
9
+ function houseRule(houses) {
10
+ return `prefix_rule(pattern = ["house"], decision = "allow")\nhost_executable(name = "house", paths = ${JSON.stringify(houses)})\n`;
11
+ }
7
12
  const RAW_SUBTYPES = [
8
13
  'background_tasks_changed',
9
14
  'task_started',
@@ -22,17 +27,42 @@ async function executable(path) {
22
27
  return false;
23
28
  }
24
29
  }
25
- async function allowHouseInCodex(path) {
30
+ async function listedHouses(file) {
31
+ let text;
32
+ try {
33
+ text = await readFile(file, 'utf8');
34
+ }
35
+ catch (error) {
36
+ if (error.code === 'ENOENT')
37
+ return [];
38
+ throw error;
39
+ }
40
+ try {
41
+ const listed = JSON.parse(HOUSE_EXECUTABLES.exec(text)?.[1] ?? '[]');
42
+ return Array.isArray(listed) ? listed.filter((house) => typeof house === 'string') : [];
43
+ }
44
+ catch {
45
+ return [];
46
+ }
47
+ }
48
+ export async function allowHouseInCodex(path) {
26
49
  const candidates = [...new Set(path.split(delimiter).filter((directory) => isAbsolute(directory)))].map((directory) => join(directory, 'house'));
27
- const houses = [];
28
- for (const candidate of candidates)
29
- if (await executable(candidate))
30
- houses.push(candidate);
31
50
  const rules = join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'rules');
51
+ const file = join(rules, 'house.rules');
52
+ const houses = [];
53
+ for (const house of [...(await listedHouses(file)), ...candidates]) {
54
+ if (!houses.includes(house) && (await executable(house)))
55
+ houses.push(house);
56
+ }
32
57
  await mkdir(rules, { recursive: true });
33
- await writeFile(join(rules, 'house.rules'), `prefix_rule(pattern = ["house"], decision = "allow")\nhost_executable(name = "house", paths = ${JSON.stringify(houses)})\n`);
58
+ const temporary = `${file}.${randomUUID()}`;
59
+ await writeFile(temporary, houseRule(houses));
60
+ await rename(temporary, file);
34
61
  }
35
62
  async function nothing() { }
63
+ function answered(error) {
64
+ return typeof error === 'object' && error !== null ? error : {};
65
+ }
36
66
  function bridgeInClaudeSandbox(bridge) {
37
67
  return process.platform === 'darwin' ? { allowUnixSockets: [bridge] } : { allowAllUnixSockets: true };
38
68
  }
@@ -50,6 +80,10 @@ export const CLIS = {
50
80
  args: [],
51
81
  fullAccess: 'agent-full-access',
52
82
  sessionMeta: () => ({}),
83
+ unknownSession: (error) => {
84
+ const { code, data } = answered(error);
85
+ return code === -32603 && typeof data?.details === 'string' && data.details.startsWith('no rollout found');
86
+ },
53
87
  renames: true,
54
88
  allowHouse: allowHouseInCodex,
55
89
  signedIn: { command: ['login', 'status'] },
@@ -79,6 +113,7 @@ export const CLIS = {
79
113
  emitRawSDKMessages: RAW_SUBTYPES.map((subtype) => ({ type: 'system', subtype })),
80
114
  },
81
115
  }),
116
+ unknownSession: (error) => answered(error).code === -32002,
82
117
  renames: false,
83
118
  allowHouse: nothing,
84
119
  signedIn: { command: ['auth', 'status'] },
@@ -93,6 +128,7 @@ export const CLIS = {
93
128
  args: ['--sandbox', 'off', 'agent', '--always-approve', '--no-leader', 'stdio'],
94
129
  fullAccess: null,
95
130
  sessionMeta: () => ({}),
131
+ unknownSession: (error) => answered(error).data?.code === 'FS_NOT_FOUND',
96
132
  renames: false,
97
133
  allowHouse: nothing,
98
134
  signedIn: { initializeMeta: 'defaultAuthMethodId' },
@@ -173,7 +209,7 @@ export async function installAdapter(kind, signal) {
173
209
  const release = KIT_PACKAGE.peerDependencies[adapter.package];
174
210
  if ((await installedAdapter(kind)) === release)
175
211
  return null;
176
- const ran = await run('npm', ['install', '--prefix', prefix(kind), '--no-save', '--no-audit', '--no-fund', '--omit=optional', `${adapter.package}@${release}`], INSTALL_MS, true, signal);
212
+ const ran = await run('npm', ['install', '--prefix', prefix(kind), '--cache', join(kitHome(), 'npm-cache'), '--no-save', '--no-audit', '--no-fund', '--omit=optional', `${adapter.package}@${release}`], INSTALL_MS, true, signal);
177
213
  return ran.status === 0 ? null : failureOf(ran, 'npm');
178
214
  }
179
215
  export async function installCli(kind, signal) {
@@ -198,6 +234,12 @@ export async function loginPath() {
198
234
  const ran = await inLoginShell(`printf "${marker}%s\\n" "$PATH"`);
199
235
  return ran.stdout.split('\n').findLast((line) => line.startsWith(marker)).slice(marker.length);
200
236
  }
237
+ export async function writeCodexRule() {
238
+ const shell = await loginPath().catch(() => '');
239
+ await allowHouseInCodex([dirname(process.execPath), process.env.PATH ?? '', shell].join(delimiter)).catch((error) => {
240
+ throw new Error(`Codex's rule for house could not be written: ${error.message}`);
241
+ });
242
+ }
201
243
  export async function versionOf(path, signal) {
202
244
  return VERSION.exec((await run(path, ['--version'], READ_MS, false, signal)).stdout)?.[0] ?? null;
203
245
  }
@@ -1,31 +1,82 @@
1
- import { client, } from '@agentclientprotocol/sdk';
1
+ import { client, RequestError, } from '@agentclientprotocol/sdk';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
3
  import { mkdir } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
5
  import { unlessAborted } from './abort.js';
6
6
  import { startAdapter } from './acp.js';
7
+ import { finalRefusal } from './api.js';
7
8
  import { openBridge } from './bridge.js';
8
9
  import { CLIS } from './clis.js';
9
- import { readerFor } from './events.js';
10
+ import { answeredFailure, readerFor } from './events.js';
10
11
  import { placeFiles, uploadBytes } from './files.js';
11
12
  import { agentBase } from './home.js';
12
13
  import { instructions } from './instructions.js';
13
14
  import { Parts } from './parts.js';
15
+ import { refusalOf } from './refusals.js';
14
16
  import { killMarked, Scope } from './scope.js';
15
17
  import { holdSecretInput } from './secret-input.js';
16
18
  import { createHowWeWork } from './skills.js';
17
19
  import { transcribe } from './transcription.js';
18
20
  import { workFor } from './work.js';
21
+ const SIGNED_OUT = RequestError.authRequired().code;
19
22
  const KILLED = 'the conversation was killed';
23
+ const REFUSED = 'the CLI refused the prompt';
24
+ const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
25
+ const GONE = 'House no longer holds this conversation; start a new one';
26
+ const RETIRED = "this conversation's Agent is retired, so it cannot start";
27
+ const ENDED = {
28
+ conversation_not_found: GONE,
29
+ conversation_historical: RETIRED,
30
+ };
31
+ const NO_START = "the Agent's CLI did not start";
32
+ const NO_SESSION = "the Agent's CLI did not open a session";
33
+ const NO_RESUME = "the Agent's CLI could not resume this conversation";
34
+ const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
35
+ class NotStarted extends Error {
36
+ }
37
+ async function step(reason, work) {
38
+ try {
39
+ return await work;
40
+ }
41
+ catch (error) {
42
+ throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
43
+ }
44
+ }
45
+ function unbridged(error) {
46
+ return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
47
+ }
48
+ const EXIT_PLAN_MODE = {
49
+ parse(value) {
50
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
51
+ throw new Error('_x.ai/exit_plan_mode needs an object');
52
+ }
53
+ return value;
54
+ },
55
+ };
56
+ export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
20
57
  function causeOf(error) {
21
58
  return error instanceof Error ? error.message : String(error);
22
59
  }
60
+ function answeredWith(error) {
61
+ const signedOut = typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
62
+ return { failure: signedOut ? 'signed_out' : 'error', cause: causeOf(error) };
63
+ }
64
+ function exitedWith(running, cause) {
65
+ return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
66
+ }
23
67
  function answerMessage(request, response) {
24
68
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
25
69
  }
26
70
  function logged(error) {
27
71
  process.stderr.write(`kit: ${causeOf(error)}\n`);
28
72
  }
73
+ function refusal(conversation, error) {
74
+ if (conversation.kills > 0)
75
+ return KILLED;
76
+ const cause = error instanceof NotStarted ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
77
+ logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
78
+ return causeOf(error);
79
+ }
29
80
  function marker(bridge) {
30
81
  return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
31
82
  }
@@ -56,6 +107,7 @@ class Conversation {
56
107
  subagents = new Map();
57
108
  held = [];
58
109
  opening = null;
110
+ prompted = null;
59
111
  kills = 0;
60
112
  killed = new AbortController();
61
113
  interrupts = 0;
@@ -78,9 +130,7 @@ function asksSecret(request) {
78
130
  export class Conversations {
79
131
  kit;
80
132
  conversations = new Map();
81
- acks = new Map();
82
133
  questions = new Map();
83
- carrying = 0;
84
134
  reporting = 0;
85
135
  watched = false;
86
136
  constructor(kit) {
@@ -89,37 +139,26 @@ export class Conversations {
89
139
  setInterval(() => this.look(), 2_000);
90
140
  }
91
141
  input(input) {
92
- this.carrying++;
93
- const carried = this.acks.get(input.input_id) ??
94
- (() => {
95
- const conversation = this.conversation(input.conversation_id);
96
- if (input.kind === 'kill') {
97
- conversation.kills++;
98
- conversation.killed.abort(new Error(KILLED));
99
- for (const waiting of conversation.waiting.splice(0))
100
- waiting.settle({ refused: KILLED });
101
- this.halt(conversation);
102
- }
103
- if (input.kind === 'interrupt') {
104
- conversation.interrupts++;
105
- conversation.interrupted.abort();
106
- }
107
- const handled = conversation.queue.then(() => this.carry(conversation, input));
108
- conversation.queue = handled.catch(() => undefined);
109
- const work = handled.then(({ ack }) => ack);
110
- this.acks.set(input.input_id, work);
111
- return work;
112
- })();
113
- carried
114
- .then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
115
- .catch(logged)
116
- .finally(() => {
117
- this.carrying--;
118
- this.kit.changed();
142
+ void this.kit.inputs.carry(input.input_id, () => {
143
+ const conversation = this.conversation(input.conversation_id);
144
+ if (input.kind === 'kill') {
145
+ conversation.kills++;
146
+ conversation.killed.abort(new Error(KILLED));
147
+ for (const waiting of conversation.waiting.splice(0))
148
+ waiting.settle({ refused: KILLED });
149
+ this.halt(conversation);
150
+ }
151
+ if (input.kind === 'interrupt') {
152
+ conversation.interrupts++;
153
+ conversation.interrupted.abort();
154
+ }
155
+ const handled = conversation.queue.then(() => this.carry(conversation, input));
156
+ conversation.queue = handled.catch(() => undefined);
157
+ return handled.then(async ({ ack }) => ({ ack: await ack }));
119
158
  });
120
159
  }
121
160
  idle() {
122
- if (this.carrying > 0 || this.reporting > 0)
161
+ if (this.reporting > 0)
123
162
  return false;
124
163
  for (const conversation of this.conversations.values()) {
125
164
  const turn = conversation.turn;
@@ -132,6 +171,9 @@ export class Conversations {
132
171
  return false;
133
172
  return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
134
173
  }
174
+ quiet() {
175
+ return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
176
+ }
135
177
  look() {
136
178
  for (const conversation of this.conversations.values()) {
137
179
  for (const scope of conversation.leftovers) {
@@ -196,10 +238,10 @@ export class Conversations {
196
238
  if (conversation.running !== null)
197
239
  return {};
198
240
  try {
199
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
241
+ return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null, true) };
200
242
  }
201
243
  catch (error) {
202
- return { refused: causeOf(error) };
244
+ return { refused: refusal(conversation, error) };
203
245
  }
204
246
  }
205
247
  async message(conversation, input) {
@@ -216,7 +258,7 @@ export class Conversations {
216
258
  throw new Error(KILLED);
217
259
  }
218
260
  catch (error) {
219
- return { ack: Promise.resolve({ refused: conversation.kills > 0 ? KILLED : causeOf(error) }) };
261
+ return { ack: Promise.resolve({ refused: refusal(conversation, error) }) };
220
262
  }
221
263
  return {
222
264
  ack: new Promise((settle) => {
@@ -239,13 +281,17 @@ export class Conversations {
239
281
  async send(conversation, waiting) {
240
282
  const { input } = waiting;
241
283
  let ack = {};
284
+ let started = false;
242
285
  const prompt = [];
243
286
  try {
244
287
  if (conversation.running === null) {
245
- const resumed = input.provider_session_id ?? conversation.session;
246
- const opened = await this.start(conversation, waiting.route, resumed);
247
- if (resumed === null)
248
- ack = { provider_session_id: opened };
288
+ const stored = conversation.session ?? input.provider_session_id;
289
+ const empty = input.first === true && conversation.prompted !== stored;
290
+ started = (await this.start(conversation, waiting.route, stored, empty)) !== stored;
291
+ }
292
+ const session = conversation.running.sessionId;
293
+ if (started || (input.first === true && session !== input.provider_session_id)) {
294
+ ack = { provider_session_id: session };
249
295
  }
250
296
  if (input.first === true) {
251
297
  prompt.push({
@@ -257,11 +303,11 @@ export class Conversations {
257
303
  throw new Error(KILLED);
258
304
  }
259
305
  catch (error) {
260
- if ('provider_session_id' in ack) {
306
+ if (started) {
261
307
  conversation.session = null;
262
308
  await this.kill(conversation);
263
309
  }
264
- waiting.settle({ refused: conversation.kills > 0 ? KILLED : causeOf(error) });
310
+ waiting.settle({ refused: refusal(conversation, error) });
265
311
  return;
266
312
  }
267
313
  this.prompt(conversation, [...prompt, ...waiting.content]);
@@ -339,14 +385,15 @@ export class Conversations {
339
385
  }
340
386
  return route;
341
387
  }
342
- async start(conversation, route, sessionId) {
388
+ async start(conversation, route, sessionId, empty) {
343
389
  const reader = readerFor(route.kind);
344
390
  reader.session = sessionId;
345
391
  const app = client({ name: '@agentshouse/kit' })
346
392
  .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
347
- .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
393
+ .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
394
+ .onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
348
395
  const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
349
- const bridge = await openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
396
+ const bridge = await step(unbridged, openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal));
350
397
  if (conversation.kills > 0) {
351
398
  bridge.close();
352
399
  throw new Error(KILLED);
@@ -354,27 +401,28 @@ export class Conversations {
354
401
  conversation.opening = bridge;
355
402
  const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
356
403
  const scope = new Scope(marker(bridge), randomUUID(), cli);
404
+ let adapter = null;
357
405
  try {
358
406
  await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
359
407
  await CLIS[route.kind].allowHouse(bridge.env.PATH);
360
- const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => {
408
+ const streamed = await step(NO_CONNECTION, work.open(cli));
409
+ adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
361
410
  work.read(message);
362
411
  this.read(conversation, reader, message);
363
- }, { ...bridge.env, ...(await work.open(cli)) }, undefined, scope);
412
+ }, { ...bridge.env, ...streamed }, undefined, scope));
364
413
  void adapter.exited.then(() => bridge.close());
365
414
  const agent = adapter.connection.agent;
366
415
  const cwd = route.working_directory;
367
416
  const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
368
- const opened = sessionId === null
369
- ? await agent.request('session/new', { cwd, mcpServers: [], _meta })
370
- : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
417
+ const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
418
+ const opened = resumed ?? (await step(NO_SESSION, agent.request('session/new', { cwd, mcpServers: [], _meta })));
371
419
  reader.session = opened.sessionId;
372
- if (sessionId === null && CLIS[route.kind].renames) {
420
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
373
421
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
374
- await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
422
+ await step(NO_SESSION, agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
375
423
  }
376
424
  const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
377
- const options = await this.launchSettings(running, route, opened.configOptions ?? []);
425
+ const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
378
426
  void work.settled().then(() => scope.record());
379
427
  conversation.running = running;
380
428
  conversation.session = running.sessionId;
@@ -382,11 +430,11 @@ export class Conversations {
382
430
  if (conversation.commands !== null)
383
431
  this.commands(conversation, conversation.commands);
384
432
  this.options(conversation, options);
385
- void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
433
+ void adapter.lost.then((cause) => this.exited(conversation, running, cause));
386
434
  return running.sessionId;
387
435
  }
388
436
  catch (error) {
389
- scope.kill();
437
+ adapter?.retire();
390
438
  work.close();
391
439
  conversation.leftovers.add(scope);
392
440
  bridge.close();
@@ -396,6 +444,18 @@ export class Conversations {
396
444
  conversation.opening = null;
397
445
  }
398
446
  }
447
+ async resume(conversation, kind, adapter, request, empty) {
448
+ try {
449
+ return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
450
+ }
451
+ catch (error) {
452
+ if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
453
+ throw new NotStarted(NO_RESUME, { cause: error });
454
+ }
455
+ logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
456
+ return null;
457
+ }
458
+ }
399
459
  async launchSettings(running, route, offered) {
400
460
  let options = offered;
401
461
  const settings = [
@@ -417,13 +477,10 @@ export class Conversations {
417
477
  }
418
478
  return options;
419
479
  }
420
- exited(conversation, adapter, cause) {
421
- const running = conversation.running;
422
- if (running?.adapter !== adapter)
423
- return;
480
+ exited(conversation, running, cause) {
424
481
  this.detach(conversation, running);
425
482
  if (conversation.turn !== null) {
426
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
483
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
427
484
  }
428
485
  this.next(conversation);
429
486
  this.kit.changed();
@@ -487,21 +544,25 @@ export class Conversations {
487
544
  this.kit.changed();
488
545
  return turn;
489
546
  }
490
- end(conversation, turn, failed) {
547
+ end(conversation, turn, failed, notice = null) {
491
548
  if (turn.ended)
492
549
  return;
550
+ if (failed !== null)
551
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
493
552
  if (conversation.turn === turn) {
494
553
  for (const event of conversation.running?.reader.closeTurn() ?? [])
495
554
  this.apply(conversation, turn, event);
496
555
  }
556
+ if (notice !== null)
557
+ turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
497
558
  turn.ended = true;
498
559
  conversation.open--;
499
560
  if (conversation.turn === turn)
500
561
  conversation.turn = null;
501
562
  const body = Promise.allSettled(turn.uploads).then(() => ({
502
- parts: turn.parts.parts,
563
+ parts: turn.parts.saved(),
503
564
  context: turn.parts.context,
504
- ...failed,
565
+ ...(failed === null ? {} : { failed: failed.failure }),
505
566
  }));
506
567
  void this.deliver(conversation, turn, 'ended', body).catch(logged);
507
568
  this.next(conversation);
@@ -511,16 +572,25 @@ export class Conversations {
511
572
  const running = conversation.running;
512
573
  const turn = this.begin(conversation);
513
574
  turn.prompted = true;
514
- running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.end(conversation, turn, null), async (error) => {
575
+ conversation.prompted = running.sessionId;
576
+ running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
577
+ const notice = answeredFailure(answer);
578
+ if (notice !== null) {
579
+ this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
580
+ }
581
+ else {
582
+ this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
583
+ }
584
+ }, async (error) => {
515
585
  if (running.adapter.connection.signal.aborted) {
516
- this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
586
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
517
587
  }
518
588
  else {
519
589
  if (conversation.open === 1 && conversation.running === running) {
520
- running.scope.kill();
590
+ running.adapter.retire();
521
591
  this.detach(conversation, running);
522
592
  }
523
- this.end(conversation, turn, { failed: causeOf(error) });
593
+ this.end(conversation, turn, answeredWith(error));
524
594
  }
525
595
  });
526
596
  }
@@ -644,7 +714,7 @@ export class Conversations {
644
714
  turn.parts.plan(event.entries);
645
715
  return;
646
716
  case 'image':
647
- turn.uploads.push(this.image(conversation, turn, event.data, event.mediaType));
717
+ turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
648
718
  this.kit.changed();
649
719
  return;
650
720
  }
@@ -664,7 +734,7 @@ export class Conversations {
664
734
  else
665
735
  conversation.turn.parts.start(part);
666
736
  }
667
- async image(conversation, turn, data, mediaType) {
737
+ async image(conversation, turn, reserved, data, mediaType) {
668
738
  const bytes = Buffer.from(data, 'base64');
669
739
  const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
670
740
  try {
@@ -677,10 +747,10 @@ export class Conversations {
677
747
  const saved = declared.upload === undefined ? declared : await uploadBytes(declared.upload, bytes);
678
748
  if (saved.save.status !== 'saved')
679
749
  throw new Error(saved.save.failure ?? saved.save.status);
680
- turn.parts.start({ type: 'image', file: saved.version, name, media_type: mediaType });
750
+ turn.parts.arrive({ type: 'image', file: saved.version, name, media_type: mediaType }, reserved);
681
751
  }
682
752
  catch (error) {
683
- turn.parts.start({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` });
753
+ turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${causeOf(error)}` }, reserved);
684
754
  }
685
755
  }
686
756
  }