@agentshouse/kit 0.2.0 → 0.2.1-alpha.10

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 (113) hide show
  1. package/README.md +20 -2
  2. package/dist/acp.js +120 -0
  3. package/dist/agents.js +96 -0
  4. package/dist/api.js +39 -3
  5. package/dist/authority-main.js +22 -0
  6. package/dist/blocks.js +51 -0
  7. package/dist/bootstrap.json +28 -0
  8. package/dist/bridge.js +214 -0
  9. package/dist/clis.js +114 -0
  10. package/dist/configure-main.js +6 -0
  11. package/dist/conversations.js +561 -0
  12. package/dist/copies.js +796 -0
  13. package/dist/enrol-main.js +4 -0
  14. package/dist/files.js +21 -0
  15. package/dist/git-main.js +153 -0
  16. package/dist/git.js +81 -0
  17. package/dist/home.js +19 -6
  18. package/dist/house-main.js +106 -3
  19. package/dist/instructions.js +33 -0
  20. package/dist/kit-main.js +2 -0
  21. package/dist/login.js +6 -2
  22. package/dist/operation.js +9 -0
  23. package/dist/resident.js +113 -2
  24. package/dist/secret-input.js +16 -0
  25. package/dist/sign-in.js +87 -0
  26. package/dist/skills.js +83 -0
  27. package/dist/stream.js +17 -1
  28. package/dist/translate.js +51 -0
  29. package/dist/update.js +116 -0
  30. package/how-we-work.md +25 -0
  31. package/package.json +29 -3
  32. package/skills/ask-matt/LICENSE +21 -0
  33. package/skills/ask-matt/PHASE-BOUNDARIES.md +55 -0
  34. package/skills/ask-matt/SKILL.md +91 -0
  35. package/skills/ask-matt/agents/openai.yaml +4 -0
  36. package/skills/code-review/LICENSE +21 -0
  37. package/skills/code-review/SKILL.md +89 -0
  38. package/skills/code-review/agents/openai.yaml +3 -0
  39. package/skills/codebase-design/DEEPENING.md +37 -0
  40. package/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  41. package/skills/codebase-design/LICENSE +21 -0
  42. package/skills/codebase-design/SKILL.md +116 -0
  43. package/skills/codebase-design/agents/openai.yaml +3 -0
  44. package/skills/diagnosing-bugs/LICENSE +21 -0
  45. package/skills/diagnosing-bugs/SKILL.md +140 -0
  46. package/skills/diagnosing-bugs/agents/openai.yaml +3 -0
  47. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
  48. package/skills/domain-modeling/ADR-FORMAT.md +47 -0
  49. package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  50. package/skills/domain-modeling/LICENSE +21 -0
  51. package/skills/domain-modeling/SKILL.md +76 -0
  52. package/skills/domain-modeling/agents/openai.yaml +3 -0
  53. package/skills/grill-me/LICENSE +21 -0
  54. package/skills/grill-me/SKILL.md +8 -0
  55. package/skills/grill-me/agents/openai.yaml +4 -0
  56. package/skills/grill-with-docs/LICENSE +21 -0
  57. package/skills/grill-with-docs/SKILL.md +8 -0
  58. package/skills/grill-with-docs/agents/openai.yaml +4 -0
  59. package/skills/grilling/LICENSE +21 -0
  60. package/skills/grilling/SKILL.md +30 -0
  61. package/skills/grilling/agents/openai.yaml +3 -0
  62. package/skills/handoff/LICENSE +21 -0
  63. package/skills/handoff/SKILL.md +17 -0
  64. package/skills/handoff/agents/openai.yaml +4 -0
  65. package/skills/implement/LICENSE +21 -0
  66. package/skills/implement/SKILL.md +16 -0
  67. package/skills/implement/agents/openai.yaml +4 -0
  68. package/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  69. package/skills/improve-codebase-architecture/LICENSE +21 -0
  70. package/skills/improve-codebase-architecture/SKILL.md +72 -0
  71. package/skills/improve-codebase-architecture/agents/openai.yaml +4 -0
  72. package/skills/prototype/LICENSE +21 -0
  73. package/skills/prototype/LOGIC.md +67 -0
  74. package/skills/prototype/SKILL.md +28 -0
  75. package/skills/prototype/UI.md +112 -0
  76. package/skills/prototype/agents/openai.yaml +3 -0
  77. package/skills/research/LICENSE +21 -0
  78. package/skills/research/SKILL.md +14 -0
  79. package/skills/research/agents/openai.yaml +3 -0
  80. package/skills/resolving-merge-conflicts/LICENSE +21 -0
  81. package/skills/resolving-merge-conflicts/SKILL.md +16 -0
  82. package/skills/resolving-merge-conflicts/agents/openai.yaml +3 -0
  83. package/skills/tdd/LICENSE +21 -0
  84. package/skills/tdd/SKILL.md +40 -0
  85. package/skills/tdd/agents/openai.yaml +3 -0
  86. package/skills/tdd/mocking.md +59 -0
  87. package/skills/tdd/tests.md +77 -0
  88. package/skills/teach/GLOSSARY-FORMAT.md +35 -0
  89. package/skills/teach/LEARNING-RECORD-FORMAT.md +46 -0
  90. package/skills/teach/LICENSE +21 -0
  91. package/skills/teach/MISSION-FORMAT.md +31 -0
  92. package/skills/teach/RESOURCES-FORMAT.md +32 -0
  93. package/skills/teach/SKILL.md +141 -0
  94. package/skills/teach/agents/openai.yaml +4 -0
  95. package/skills/to-questionnaire/LICENSE +21 -0
  96. package/skills/to-questionnaire/SKILL.md +55 -0
  97. package/skills/to-questionnaire/agents/openai.yaml +4 -0
  98. package/skills/to-spec/LICENSE +21 -0
  99. package/skills/to-spec/SKILL.md +76 -0
  100. package/skills/to-spec/agents/openai.yaml +4 -0
  101. package/skills/to-tickets/LICENSE +21 -0
  102. package/skills/to-tickets/SKILL.md +106 -0
  103. package/skills/to-tickets/agents/openai.yaml +4 -0
  104. package/skills/wait-what/LICENSE +21 -0
  105. package/skills/wait-what/SKILL.md +8 -0
  106. package/skills/wait-what/agents/openai.yaml +4 -0
  107. package/skills/wayfinder/LICENSE +21 -0
  108. package/skills/wayfinder/SKILL.md +129 -0
  109. package/skills/wayfinder/agents/openai.yaml +4 -0
  110. package/skills/writing-for-agents/LICENSE +21 -0
  111. package/skills/writing-for-agents/SKILL-MECHANICS.md +22 -0
  112. package/skills/writing-for-agents/SKILL.md +83 -0
  113. package/skills/writing-for-agents/agents/openai.yaml +3 -0
package/dist/clis.js ADDED
@@ -0,0 +1,114 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { kitHome } from './home.js';
6
+ const RUNNING_JOB = new Set(['running', 'paused']);
7
+ function threadStatus({ method, params }) {
8
+ if (method !== 'session/update' || params?.update?.sessionUpdate !== 'session_info_update')
9
+ return undefined;
10
+ return params.update._meta?.codex?.threadStatus?.type;
11
+ }
12
+ function asyncTask({ method, params }) {
13
+ const update = params?.update;
14
+ if (method !== 'session/update' || update?.asyncTaskId === undefined)
15
+ return null;
16
+ if (update.sessionUpdate === 'async_task_spawned')
17
+ return { id: update.asyncTaskId, running: true };
18
+ if (update.sessionUpdate === 'async_task_state_update' && !RUNNING_JOB.has(update.state ?? '')) {
19
+ return { id: update.asyncTaskId, running: false };
20
+ }
21
+ return null;
22
+ }
23
+ export const CLIS = {
24
+ 'codex-acp': {
25
+ package: '@agentclientprotocol/codex-acp',
26
+ bin: 'codex-acp',
27
+ args: [],
28
+ signedIn: { command: ['cli', 'login', 'status'] },
29
+ login: { args: ['cli', 'login', '--device-auth'], code: 'show' },
30
+ queues: false,
31
+ turnStarted: (notice) => threadStatus(notice) === 'active',
32
+ turnEnded: (notice) => threadStatus(notice) === 'idle',
33
+ job: asyncTask,
34
+ },
35
+ 'claude-agent-acp': {
36
+ package: '@agentclientprotocol/claude-agent-acp',
37
+ bin: 'claude-agent-acp',
38
+ args: [],
39
+ signedIn: { command: ['--cli', 'auth', 'status'] },
40
+ login: { args: ['--cli', 'auth', 'login', '--claudeai'], code: 'collect' },
41
+ queues: false,
42
+ turnStarted: () => false,
43
+ turnEnded: ({ method, params }) => method === 'session/update' &&
44
+ params?.update?.sessionUpdate === 'usage_update' &&
45
+ params.update._meta?.['_claude/origin'] !== undefined,
46
+ job: asyncTask,
47
+ },
48
+ 'grok-build': {
49
+ package: '@xai-official/grok',
50
+ bin: 'grok',
51
+ args: ['agent', '--no-leader', 'stdio'],
52
+ signedIn: { initializeMeta: 'defaultAuthMethodId' },
53
+ login: { args: ['login', '--device-auth'], code: 'show' },
54
+ queues: true,
55
+ turnStarted: ({ method, params }) => method === '_x.ai/queue/changed' && params?.runningPromptId !== undefined,
56
+ turnEnded: ({ method, params }) => method === '_x.ai/session_notification' && params?.update?.sessionUpdate === 'turn_completed',
57
+ job: ({ method, params }) => {
58
+ if (method === '_x.ai/task_backgrounded')
59
+ return { id: params.update.task_id, running: true };
60
+ if (method === '_x.ai/task_completed')
61
+ return { id: params.update.task_snapshot.task_id, running: false };
62
+ return null;
63
+ },
64
+ },
65
+ };
66
+ const KIT_PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
67
+ export const KIT_VERSION = KIT_PACKAGE.version;
68
+ const PROXY = /^https?_proxy$/i;
69
+ export function pinned(kind) {
70
+ return KIT_PACKAGE.peerDependencies[CLIS[kind].package];
71
+ }
72
+ function prefix(kind) {
73
+ return join(kitHome(), 'agents', kind);
74
+ }
75
+ export function cliCommand(kind) {
76
+ return join(prefix(kind), 'node_modules', '.bin', CLIS[kind].bin);
77
+ }
78
+ export async function installedRelease(kind) {
79
+ try {
80
+ const manifest = join(prefix(kind), 'node_modules', CLIS[kind].package, 'package.json');
81
+ return JSON.parse(await readFile(manifest, 'utf8')).version;
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ export function withoutProxy() {
88
+ return Object.fromEntries(Object.entries(process.env).filter(([name]) => !PROXY.test(name)));
89
+ }
90
+ export function run(command, args, timeoutMs) {
91
+ return new Promise((resolve) => {
92
+ const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], timeout: timeoutMs, env: withoutProxy() });
93
+ let output = '';
94
+ child.stdout.on('data', (chunk) => {
95
+ output += chunk.toString('utf8');
96
+ });
97
+ child.stderr.on('data', (chunk) => {
98
+ output += chunk.toString('utf8');
99
+ });
100
+ child.on('error', (error) => resolve({ status: null, output: error.message }));
101
+ child.on('close', (status) => resolve({ status, output }));
102
+ });
103
+ }
104
+ export async function install(kind) {
105
+ const release = pinned(kind);
106
+ if ((await installedRelease(kind)) === release)
107
+ return release;
108
+ const ran = await run('npm', ['install', '--prefix', prefix(kind), '--no-save', '--no-audit', '--no-fund', `${CLIS[kind].package}@${release}`], 15 * 60_000);
109
+ if (ran.status !== 0) {
110
+ process.stderr.write(`kit: ${kind} did not install: ${ran.output.trim()}\n`);
111
+ return null;
112
+ }
113
+ return installedRelease(kind);
114
+ }
@@ -0,0 +1,6 @@
1
+ import { readConfiguration, writeConfiguration } from './home.js';
2
+ const skills = !process.argv.slice(2).includes('--no-skills');
3
+ const stored = await readConfiguration();
4
+ await writeConfiguration({ skills });
5
+ if (stored.skills !== skills)
6
+ process.stdout.write('changed\n');
@@ -0,0 +1,561 @@
1
+ import { client, } from '@agentclientprotocol/sdk';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { mkdir } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { killMarked, startAdapter, TURN_ENDED, TURN_STARTED } from './acp.js';
6
+ import { blocksOf, firstChange } from './blocks.js';
7
+ import { openBridge } from './bridge.js';
8
+ import { CLIS } from './clis.js';
9
+ import { placeFiles } from './files.js';
10
+ import { instructions } from './instructions.js';
11
+ import { holdSecretInput } from './secret-input.js';
12
+ import { createHowWeWork } from './skills.js';
13
+ const LAUNCH_BASE = '/agents/house';
14
+ const KILLED = 'the conversation was killed';
15
+ function causeOf(error) {
16
+ return error instanceof Error ? error.message : String(error);
17
+ }
18
+ function answerMessage(request, response) {
19
+ return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
20
+ }
21
+ function logged(error) {
22
+ process.stderr.write(`kit: ${causeOf(error)}\n`);
23
+ }
24
+ function unlessKilled(work, signal) {
25
+ return new Promise((resolve, reject) => {
26
+ const killed = () => reject(signal.reason);
27
+ signal.addEventListener('abort', killed, { once: true });
28
+ if (signal.aborted)
29
+ killed();
30
+ work.then(resolve, reject).finally(() => signal.removeEventListener('abort', killed));
31
+ });
32
+ }
33
+ function stop(bridge) {
34
+ killMarked(`HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`);
35
+ }
36
+ class Turn {
37
+ id = randomUUID();
38
+ text = '';
39
+ sent = [];
40
+ unsentFrom = null;
41
+ draftSequence = 0;
42
+ planSequence = 0;
43
+ entries = null;
44
+ questions = 0;
45
+ secrets = 0;
46
+ prompt = null;
47
+ ended = false;
48
+ }
49
+ class Conversation {
50
+ id;
51
+ running = null;
52
+ turn = null;
53
+ commands = null;
54
+ jobs = new Set();
55
+ queued = [];
56
+ late = [];
57
+ opening = null;
58
+ kills = 0;
59
+ killed = new AbortController();
60
+ open = 0;
61
+ queue = Promise.resolve();
62
+ reports = Promise.resolve();
63
+ constructor(id) {
64
+ this.id = id;
65
+ }
66
+ }
67
+ function formProperties(request) {
68
+ if (request.mode !== 'form')
69
+ return {};
70
+ return request.requestedSchema?.properties ?? {};
71
+ }
72
+ function asksSecret(request) {
73
+ return Object.values(formProperties(request)).some((property) => Object.values(property._meta ?? {}).some((meta) => typeof meta === 'object' && meta !== null && meta.isSecret === true));
74
+ }
75
+ export class Conversations {
76
+ kit;
77
+ conversations = new Map();
78
+ acks = new Map();
79
+ questions = new Map();
80
+ carrying = 0;
81
+ reporting = 0;
82
+ constructor(kit) {
83
+ this.kit = kit;
84
+ }
85
+ input(input) {
86
+ this.carrying++;
87
+ const carried = this.acks.get(input.input_id) ??
88
+ (() => {
89
+ const conversation = this.conversation(input.conversation_id);
90
+ if (input.kind === 'kill') {
91
+ conversation.kills++;
92
+ conversation.killed.abort(new Error(KILLED));
93
+ this.halt(conversation);
94
+ }
95
+ const work = conversation.queue.then(() => this.carry(conversation, input));
96
+ conversation.queue = work.catch(() => undefined);
97
+ this.acks.set(input.input_id, work);
98
+ return work;
99
+ })();
100
+ carried
101
+ .then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
102
+ .catch(logged)
103
+ .finally(() => {
104
+ this.carrying--;
105
+ this.kit.changed();
106
+ });
107
+ }
108
+ idle() {
109
+ if (this.carrying > 0 || this.reporting > 0)
110
+ return false;
111
+ for (const conversation of this.conversations.values()) {
112
+ const turn = conversation.turn;
113
+ const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
114
+ if (conversation.jobs.size > 0 || conversation.open > (asking ? 1 : 0))
115
+ return false;
116
+ }
117
+ return true;
118
+ }
119
+ opened() {
120
+ for (const conversation of this.conversations.values()) {
121
+ if (conversation.turn !== null)
122
+ conversation.turn.unsentFrom = 0;
123
+ }
124
+ }
125
+ conversation(id) {
126
+ let conversation = this.conversations.get(id);
127
+ if (conversation === undefined) {
128
+ conversation = new Conversation(id);
129
+ this.conversations.set(id, conversation);
130
+ }
131
+ return conversation;
132
+ }
133
+ async carry(conversation, input) {
134
+ if (input.kind === 'open')
135
+ return this.open(conversation, input);
136
+ if (input.kind === 'message')
137
+ return this.message(conversation, input);
138
+ if (input.kind === 'interrupt')
139
+ await this.interrupt(conversation, String(input.turn_id));
140
+ if (input.kind === 'kill') {
141
+ await this.kill(conversation);
142
+ if (--conversation.kills === 0)
143
+ conversation.killed = new AbortController();
144
+ }
145
+ if (input.kind === 'option')
146
+ await this.option(conversation, String(input.option), input.value);
147
+ if (input.kind === 'answer') {
148
+ const question = this.questions.get(String(input.interaction_id));
149
+ if (question === undefined) {
150
+ return this.message(conversation, { ...input, text: answerMessage(input.request, input.response), files: [], first: false });
151
+ }
152
+ question.answer(input.response);
153
+ }
154
+ return {};
155
+ }
156
+ async open(conversation, input) {
157
+ if (conversation.running !== null)
158
+ return {};
159
+ try {
160
+ return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
161
+ }
162
+ catch (error) {
163
+ return { refused: causeOf(error) };
164
+ }
165
+ }
166
+ async message(conversation, input) {
167
+ let ack = {};
168
+ const prompt = [];
169
+ try {
170
+ const route = await this.route(conversation, input.agent_id);
171
+ const paths = await placeFiles(this.kit.house, route.working_directory, input.files, conversation.killed.signal);
172
+ if (conversation.running === null) {
173
+ const opened = await this.start(conversation, route, input.provider_session_id);
174
+ if (input.provider_session_id === null)
175
+ ack = { provider_session_id: opened };
176
+ }
177
+ if (input.first === true) {
178
+ prompt.push({
179
+ type: 'text',
180
+ text: await instructions(conversation.running.bridge, route.base_instructions, await this.kit.copies.mapping()),
181
+ });
182
+ }
183
+ prompt.push({ type: 'text', text: String(input.text) });
184
+ if (paths.length > 0)
185
+ prompt.push({ type: 'text', text: paths.join('\n') });
186
+ if (conversation.kills > 0)
187
+ throw new Error(KILLED);
188
+ }
189
+ catch (error) {
190
+ if ('provider_session_id' in ack)
191
+ await this.kill(conversation);
192
+ return { refused: conversation.kills > 0 ? KILLED : causeOf(error) };
193
+ }
194
+ this.prompt(conversation, prompt);
195
+ return ack;
196
+ }
197
+ async interrupt(conversation, turnId) {
198
+ if (conversation.running === null || conversation.turn?.id !== turnId)
199
+ return;
200
+ await conversation.running.adapter.connection.agent.notify('session/cancel', {
201
+ sessionId: conversation.running.sessionId,
202
+ });
203
+ }
204
+ halt(conversation) {
205
+ if (conversation.opening !== null)
206
+ stop(conversation.opening);
207
+ if (conversation.running === null)
208
+ return;
209
+ conversation.running.killed = true;
210
+ stop(conversation.running.bridge);
211
+ }
212
+ async kill(conversation) {
213
+ const running = conversation.running;
214
+ this.halt(conversation);
215
+ await running?.adapter.exited;
216
+ }
217
+ async option(conversation, option, value) {
218
+ const running = conversation.running;
219
+ if (running === null)
220
+ return;
221
+ try {
222
+ const answered = await running.adapter.connection.agent.request('session/set_config_option', {
223
+ sessionId: running.sessionId,
224
+ configId: option,
225
+ ...(typeof value === 'boolean' ? { type: 'boolean', value } : { value: String(value) }),
226
+ });
227
+ this.options(conversation, answered.configOptions);
228
+ }
229
+ catch (error) {
230
+ logged(error);
231
+ }
232
+ }
233
+ async route(conversation, agentId) {
234
+ const signal = conversation.killed.signal;
235
+ await unlessKilled(this.kit.agents.read(), signal);
236
+ if (this.kit.agents.route(agentId) === undefined)
237
+ await unlessKilled(this.kit.agents.refresh(), signal);
238
+ const route = this.kit.agents.route(agentId);
239
+ if (route === undefined)
240
+ throw new Error(`this Environment hosts no Agent ${agentId}`);
241
+ if (route.working_directory === join(LAUNCH_BASE, route.agent_id)) {
242
+ await mkdir(route.working_directory, { recursive: true });
243
+ }
244
+ return route;
245
+ }
246
+ async start(conversation, route, sessionId) {
247
+ const app = client({ name: '@agentshouse/kit' })
248
+ .onNotification('session/update', ({ params }) => this.update(conversation, params))
249
+ .onNotification(TURN_STARTED, (params) => params, () => void this.started(conversation))
250
+ .onNotification(TURN_ENDED, (params) => params, () => this.ended(conversation))
251
+ .onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
252
+ .onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal));
253
+ await unlessKilled(this.kit.agents.installed(), conversation.killed.signal);
254
+ const bridge = await openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
255
+ if (conversation.kills > 0) {
256
+ bridge.close();
257
+ throw new Error(KILLED);
258
+ }
259
+ conversation.opening = bridge;
260
+ try {
261
+ await unlessKilled(createHowWeWork(bridge), conversation.killed.signal);
262
+ const adapter = await startAdapter(route.kind, route.working_directory, app, (job) => this.job(conversation, job), bridge.env);
263
+ void adapter.exited.then(() => bridge.close());
264
+ const agent = adapter.connection.agent;
265
+ const cwd = route.working_directory;
266
+ const opened = sessionId === null
267
+ ? await agent.request('session/new', { cwd, mcpServers: [] })
268
+ : { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [] })), sessionId };
269
+ const running = {
270
+ adapter,
271
+ bridge,
272
+ sessionId: opened.sessionId,
273
+ killed: false,
274
+ queues: CLIS[route.kind].queues,
275
+ };
276
+ const options = await this.launchSettings(running, route, opened.configOptions ?? []);
277
+ conversation.running = running;
278
+ this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
279
+ if (conversation.commands !== null)
280
+ this.commands(conversation, conversation.commands);
281
+ this.options(conversation, options);
282
+ void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
283
+ return running.sessionId;
284
+ }
285
+ catch (error) {
286
+ stop(bridge);
287
+ bridge.close();
288
+ throw conversation.kills > 0 ? new Error(KILLED) : error;
289
+ }
290
+ finally {
291
+ conversation.opening = null;
292
+ }
293
+ }
294
+ async launchSettings(running, route, offered) {
295
+ let options = offered;
296
+ const settings = [
297
+ ['model', route.model],
298
+ ['thought_level', route.effort],
299
+ ];
300
+ for (const [category, value] of settings) {
301
+ if (value === null)
302
+ continue;
303
+ const option = options.find((candidate) => candidate.category === category);
304
+ const answered = await running.adapter.connection.agent.request('session/set_config_option', {
305
+ sessionId: running.sessionId,
306
+ configId: option?.id ?? category,
307
+ value,
308
+ });
309
+ options = answered.configOptions;
310
+ }
311
+ return options;
312
+ }
313
+ exited(conversation, adapter, cause) {
314
+ const running = conversation.running;
315
+ if (running?.adapter !== adapter)
316
+ return;
317
+ conversation.running = null;
318
+ conversation.commands = null;
319
+ conversation.jobs.clear();
320
+ conversation.queued.length = 0;
321
+ for (const [id, question] of this.questions) {
322
+ if (question.conversation === conversation)
323
+ this.questions.delete(id);
324
+ }
325
+ this.kit.send({ type: 'process', conversation_id: conversation.id, running: false });
326
+ if (conversation.turn !== null) {
327
+ this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
328
+ }
329
+ this.kit.changed();
330
+ }
331
+ commands(conversation, commands) {
332
+ conversation.commands = commands;
333
+ if (conversation.running === null)
334
+ return;
335
+ this.kit.send({ type: 'commands', conversation_id: conversation.id, commands });
336
+ }
337
+ options(conversation, options) {
338
+ if (conversation.running === null)
339
+ return;
340
+ this.kit.send({ type: 'options', conversation_id: conversation.id, options });
341
+ }
342
+ deliver(conversation, turn, event, body) {
343
+ this.reporting++;
344
+ const delivered = conversation.reports.then(() => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, body));
345
+ conversation.reports = delivered
346
+ .catch(() => undefined)
347
+ .finally(() => {
348
+ this.reporting--;
349
+ this.kit.changed();
350
+ });
351
+ return delivered;
352
+ }
353
+ report(conversation) {
354
+ const turn = new Turn();
355
+ conversation.open++;
356
+ void this.deliver(conversation, turn, 'started', {})
357
+ .then(() => this.admitted(conversation, turn))
358
+ .catch(logged);
359
+ return turn;
360
+ }
361
+ admitted(conversation, turn) {
362
+ if (turn.text !== '') {
363
+ turn.unsentFrom = 0;
364
+ this.draft(conversation, turn);
365
+ }
366
+ if (turn.entries !== null)
367
+ this.plan(conversation, turn);
368
+ }
369
+ begin(conversation) {
370
+ const turn = this.report(conversation);
371
+ conversation.turn = turn;
372
+ this.kit.changed();
373
+ return turn;
374
+ }
375
+ end(conversation, turn, outcome) {
376
+ if (turn.ended)
377
+ return;
378
+ turn.ended = true;
379
+ conversation.open--;
380
+ if (conversation.turn === turn)
381
+ conversation.turn = null;
382
+ void this.deliver(conversation, turn, 'ended', outcome).catch(logged);
383
+ const late = conversation.open === 0 ? conversation.late.shift() : undefined;
384
+ if (late !== undefined)
385
+ this.failed(conversation, late);
386
+ this.kit.changed();
387
+ }
388
+ failed(conversation, cause) {
389
+ if (conversation.open > 0)
390
+ conversation.late.push(cause);
391
+ else
392
+ this.end(conversation, this.report(conversation), { failed: cause });
393
+ }
394
+ prompt(conversation, prompt) {
395
+ const running = conversation.running;
396
+ const sent = { turn: null };
397
+ if (running.queues && (conversation.turn !== null || conversation.queued.length > 0)) {
398
+ conversation.queued.push(sent);
399
+ }
400
+ else {
401
+ this.attach(conversation.turn ?? this.begin(conversation), sent);
402
+ }
403
+ running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then(() => this.answered(conversation, sent, null), async (error) => {
404
+ if (!running.adapter.connection.signal.aborted)
405
+ this.answered(conversation, sent, causeOf(error));
406
+ else if (sent.turn !== null) {
407
+ this.end(conversation, sent.turn, { failed: running.killed ? KILLED : await running.adapter.exited });
408
+ }
409
+ });
410
+ }
411
+ attach(turn, sent) {
412
+ turn.prompt = sent;
413
+ sent.turn = turn;
414
+ }
415
+ answered(conversation, sent, failed) {
416
+ const turn = sent.turn;
417
+ if (turn === null) {
418
+ const queued = conversation.queued.indexOf(sent);
419
+ if (queued < 0)
420
+ return;
421
+ conversation.queued.splice(queued, 1);
422
+ if (failed !== null)
423
+ this.failed(conversation, failed);
424
+ }
425
+ else if (failed !== null) {
426
+ if (turn.ended)
427
+ this.failed(conversation, failed);
428
+ else
429
+ this.end(conversation, turn, { failed });
430
+ }
431
+ else if (turn.prompt === sent) {
432
+ this.end(conversation, turn, { text: turn.text });
433
+ }
434
+ }
435
+ started(conversation) {
436
+ if (conversation.turn !== null)
437
+ return conversation.turn;
438
+ const turn = this.begin(conversation);
439
+ const queued = conversation.queued.shift();
440
+ if (queued !== undefined)
441
+ this.attach(turn, queued);
442
+ return turn;
443
+ }
444
+ ended(conversation) {
445
+ const turn = conversation.turn;
446
+ if (turn === null)
447
+ return;
448
+ if (turn.prompt === null) {
449
+ this.end(conversation, turn, { text: turn.text });
450
+ }
451
+ else {
452
+ conversation.turn = null;
453
+ this.kit.changed();
454
+ }
455
+ }
456
+ async ask(conversation, method, params, secret, signal) {
457
+ const turn = this.started(conversation);
458
+ const interactionId = randomUUID();
459
+ const answered = new Promise((answer, fail) => {
460
+ this.questions.set(interactionId, { conversation, answer: (response) => answer(response) });
461
+ signal.addEventListener('abort', () => answer(null), { once: true });
462
+ this.deliver(conversation, turn, 'interactions', {
463
+ interaction_id: interactionId,
464
+ request: { method, params },
465
+ secret,
466
+ }).then(() => {
467
+ if (secret)
468
+ void this.holdSecret(interactionId, params);
469
+ }, fail);
470
+ });
471
+ turn.questions++;
472
+ if (secret)
473
+ turn.secrets++;
474
+ this.kit.changed();
475
+ try {
476
+ const response = await answered;
477
+ if (response === null)
478
+ throw signal.reason;
479
+ return response;
480
+ }
481
+ finally {
482
+ this.questions.delete(interactionId);
483
+ turn.questions--;
484
+ if (secret)
485
+ turn.secrets--;
486
+ this.kit.changed();
487
+ }
488
+ }
489
+ async holdSecret(interactionId, request) {
490
+ const steps = Object.entries(formProperties(request)).map(([name, property]) => ({
491
+ kind: 'collect',
492
+ label: property.title ?? name,
493
+ name,
494
+ ...(property.description ? { description: property.description } : {}),
495
+ }));
496
+ const content = await holdSecretInput(this.kit.house, interactionId, steps, () => this.questions.has(interactionId));
497
+ if (content !== null)
498
+ this.questions.get(interactionId)?.answer({ action: 'accept', content });
499
+ }
500
+ update(conversation, notification) {
501
+ const update = notification.update;
502
+ if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
503
+ const turn = this.started(conversation);
504
+ turn.text += update.content.text;
505
+ this.draft(conversation, turn);
506
+ }
507
+ else if (update.sessionUpdate === 'plan') {
508
+ const turn = this.started(conversation);
509
+ turn.entries = update.entries;
510
+ this.plan(conversation, turn);
511
+ }
512
+ else if (update.sessionUpdate === 'agent_thought_chunk' || update.sessionUpdate === 'tool_call') {
513
+ this.started(conversation);
514
+ }
515
+ else if (update.sessionUpdate === 'available_commands_update') {
516
+ this.commands(conversation, update.availableCommands);
517
+ }
518
+ else if (update.sessionUpdate === 'config_option_update') {
519
+ this.options(conversation, update.configOptions);
520
+ }
521
+ }
522
+ job(conversation, job) {
523
+ if (job.running)
524
+ conversation.jobs.add(job.id);
525
+ else
526
+ conversation.jobs.delete(job.id);
527
+ this.kit.changed();
528
+ }
529
+ draft(conversation, turn) {
530
+ const blocks = blocksOf(turn.text);
531
+ const from = Math.min(firstChange(turn.sent, blocks), turn.unsentFrom ?? Infinity);
532
+ const sent = this.kit.send({
533
+ type: 'draft',
534
+ conversation_id: conversation.id,
535
+ turn_id: turn.id,
536
+ sequence: turn.draftSequence + 1,
537
+ from,
538
+ blocks: blocks.slice(from),
539
+ });
540
+ if (sent) {
541
+ turn.draftSequence++;
542
+ turn.sent = blocks;
543
+ turn.unsentFrom = null;
544
+ }
545
+ else {
546
+ turn.unsentFrom = from;
547
+ }
548
+ }
549
+ plan(conversation, turn) {
550
+ const sent = this.kit.send({
551
+ type: 'plan',
552
+ conversation_id: conversation.id,
553
+ turn_id: turn.id,
554
+ sequence: turn.planSequence + 1,
555
+ from: 0,
556
+ steps: turn.entries.map((entry) => ({ label: entry.content, priority: entry.priority, status: entry.status })),
557
+ });
558
+ if (sent)
559
+ turn.planSequence++;
560
+ }
561
+ }