@agentshouse/kit 0.2.1-c64c31420c9bf → 0.2.1-c70284f7a7466

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.
package/README.md CHANGED
@@ -45,11 +45,11 @@ longer act as you here; a later `kit login` reconnects the same Environment, or
45
45
  this computer as a new one when House no longer holds it.
46
46
  In a container you do not control, such as ChatGPT's, `npm i -g @agentshouse/kit` and `kit chatgpt --key <key>` with a key made on Connections let `house <tool>` act as you, starting and configuring nothing else.
47
47
 
48
- `kit resident` keeps the Environment connected and, as it connects, copies the text of each Room
49
- you chose for it as a Git repository in the workspace's `local-copies`. `house fetch` records
50
- House's changes on each copy's `refs/house/received`; rebase onto it, commit and run
51
- `house push --owner` in the copy, through the install command in the container. An Agent runs
52
- both, without `--owner`. `house upload` and `house download` move a Room's other files.
48
+ `kit resident` keeps the Environment connected and, as it connects, copies the text of each Room you chose for it as a
49
+ Git repository in the workspace's `local-copies`, and erases a Room's copy, unpushed commits included, once the Room
50
+ may no longer be copied here. `house fetch` does the same and records House's changes on each copy's
51
+ `refs/house/received`; rebase onto it, commit and run `house push --owner` in the copy, through the install command in
52
+ the container. An Agent runs both, without `--owner`. `house upload` and `house download` move a Room's other files.
53
53
 
54
54
  House Kit turns each audio file of your message to an Agent into text on this
55
55
  computer before the Agent reads it. While transcription is on, the speech
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "version": "0.2.1-c64c31420c9bf",
3
+ "version": "0.2.1-c70284f7a7466",
4
4
  "hosts": {
5
5
  "macos": {
6
6
  "asset": {
7
7
  "name": "connect-linux.sh",
8
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c64c31420c9bf/connect-linux.sh"
8
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c70284f7a7466/connect-linux.sh"
9
9
  }
10
10
  },
11
11
  "linux": {
12
12
  "asset": {
13
13
  "name": "connect-linux.sh",
14
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c64c31420c9bf/connect-linux.sh"
14
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c70284f7a7466/connect-linux.sh"
15
15
  }
16
16
  },
17
17
  "windows": {
18
18
  "asset": {
19
19
  "name": "connect-windows.ps1",
20
- "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c64c31420c9bf/connect-windows.ps1"
20
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.2.1-c70284f7a7466/connect-windows.ps1"
21
21
  }
22
22
  }
23
23
  }
package/dist/clis.js CHANGED
@@ -60,6 +60,9 @@ export async function allowHouseInCodex(path) {
60
60
  await rename(temporary, file);
61
61
  }
62
62
  async function nothing() { }
63
+ function answered(error) {
64
+ return typeof error === 'object' && error !== null ? error : {};
65
+ }
63
66
  function bridgeInClaudeSandbox(bridge) {
64
67
  return process.platform === 'darwin' ? { allowUnixSockets: [bridge] } : { allowAllUnixSockets: true };
65
68
  }
@@ -77,6 +80,10 @@ export const CLIS = {
77
80
  args: [],
78
81
  fullAccess: 'agent-full-access',
79
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
+ },
80
87
  renames: true,
81
88
  allowHouse: allowHouseInCodex,
82
89
  signedIn: { command: ['login', 'status'] },
@@ -106,6 +113,7 @@ export const CLIS = {
106
113
  emitRawSDKMessages: RAW_SUBTYPES.map((subtype) => ({ type: 'system', subtype })),
107
114
  },
108
115
  }),
116
+ unknownSession: (error) => answered(error).code === -32002,
109
117
  renames: false,
110
118
  allowHouse: nothing,
111
119
  signedIn: { command: ['auth', 'status'] },
@@ -120,6 +128,7 @@ export const CLIS = {
120
128
  args: ['--sandbox', 'off', 'agent', '--always-approve', '--no-leader', 'stdio'],
121
129
  fullAccess: null,
122
130
  sessionMeta: () => ({}),
131
+ unknownSession: (error) => answered(error).data?.code === 'FS_NOT_FOUND',
123
132
  renames: false,
124
133
  allowHouse: nothing,
125
134
  signedIn: { initializeMeta: 'defaultAuthMethodId' },
@@ -1,4 +1,4 @@
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';
@@ -16,6 +16,7 @@ import { holdSecretInput } from './secret-input.js';
16
16
  import { createHowWeWork } from './skills.js';
17
17
  import { transcribe } from './transcription.js';
18
18
  import { workFor } from './work.js';
19
+ const SIGNED_OUT = RequestError.authRequired().code;
19
20
  const KILLED = 'the conversation was killed';
20
21
  const REFUSED = 'the CLI refused the prompt';
21
22
  const NO_CONNECTION = "the Kit could not open this conversation's connection";
@@ -45,6 +46,13 @@ export const CONVERSATION_INPUTS = new Set(['open', 'message', 'interrupt', 'kil
45
46
  function causeOf(error) {
46
47
  return error instanceof Error ? error.message : String(error);
47
48
  }
49
+ function answeredWith(error) {
50
+ const signedOut = typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
51
+ return { failure: signedOut ? 'signed_out' : 'error', cause: causeOf(error) };
52
+ }
53
+ function exitedWith(running, cause) {
54
+ return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
55
+ }
48
56
  function answerMessage(request, response) {
49
57
  return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
50
58
  }
@@ -88,6 +96,7 @@ class Conversation {
88
96
  subagents = new Map();
89
97
  held = [];
90
98
  opening = null;
99
+ prompted = null;
91
100
  kills = 0;
92
101
  killed = new AbortController();
93
102
  interrupts = 0;
@@ -231,7 +240,7 @@ export class Conversations {
231
240
  if (conversation.running !== null)
232
241
  return {};
233
242
  try {
234
- return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null) };
243
+ return { provider_session_id: await this.start(conversation, await this.route(conversation, input.agent_id), null, true) };
235
244
  }
236
245
  catch (error) {
237
246
  return { refused: refusal(conversation, error) };
@@ -274,13 +283,17 @@ export class Conversations {
274
283
  async send(conversation, waiting) {
275
284
  const { input } = waiting;
276
285
  let ack = {};
286
+ let started = false;
277
287
  const prompt = [];
278
288
  try {
279
289
  if (conversation.running === null) {
280
- const resumed = input.provider_session_id ?? conversation.session;
281
- const opened = await this.start(conversation, waiting.route, resumed);
282
- if (resumed === null)
283
- ack = { provider_session_id: opened };
290
+ const stored = conversation.session ?? input.provider_session_id;
291
+ const empty = input.first === true && conversation.prompted !== stored;
292
+ started = (await this.start(conversation, waiting.route, stored, empty)) !== stored;
293
+ }
294
+ const session = conversation.running.sessionId;
295
+ if (started || (input.first === true && session !== input.provider_session_id)) {
296
+ ack = { provider_session_id: session };
284
297
  }
285
298
  if (input.first === true) {
286
299
  prompt.push({
@@ -292,7 +305,7 @@ export class Conversations {
292
305
  throw new Error(KILLED);
293
306
  }
294
307
  catch (error) {
295
- if ('provider_session_id' in ack) {
308
+ if (started) {
296
309
  conversation.session = null;
297
310
  await this.kill(conversation);
298
311
  }
@@ -374,7 +387,7 @@ export class Conversations {
374
387
  }
375
388
  return route;
376
389
  }
377
- async start(conversation, route, sessionId) {
390
+ async start(conversation, route, sessionId, empty) {
378
391
  const reader = readerFor(route.kind);
379
392
  reader.session = sessionId;
380
393
  const app = client({ name: '@agentshouse/kit' })
@@ -402,11 +415,10 @@ export class Conversations {
402
415
  const agent = adapter.connection.agent;
403
416
  const cwd = route.working_directory;
404
417
  const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
405
- const opened = sessionId === null
406
- ? await step(NO_SESSION, agent.request('session/new', { cwd, mcpServers: [], _meta }))
407
- : { ...(await step(NO_RESUME, agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta }))), sessionId };
418
+ const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
419
+ const opened = resumed ?? (await step(NO_SESSION, agent.request('session/new', { cwd, mcpServers: [], _meta })));
408
420
  reader.session = opened.sessionId;
409
- if (sessionId === null && CLIS[route.kind].renames) {
421
+ if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
410
422
  const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
411
423
  await step(NO_SESSION, agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
412
424
  }
@@ -433,6 +445,18 @@ export class Conversations {
433
445
  conversation.opening = null;
434
446
  }
435
447
  }
448
+ async resume(conversation, kind, adapter, request, empty) {
449
+ try {
450
+ return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
451
+ }
452
+ catch (error) {
453
+ if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
454
+ throw new NotStarted(NO_RESUME, { cause: error });
455
+ }
456
+ logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
457
+ return null;
458
+ }
459
+ }
436
460
  async launchSettings(running, route, offered) {
437
461
  let options = offered;
438
462
  const settings = [
@@ -460,7 +484,7 @@ export class Conversations {
460
484
  return;
461
485
  this.detach(conversation, running);
462
486
  if (conversation.turn !== null) {
463
- this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
487
+ this.end(conversation, conversation.turn, exitedWith(running, cause));
464
488
  }
465
489
  this.next(conversation);
466
490
  this.kit.changed();
@@ -527,6 +551,8 @@ export class Conversations {
527
551
  end(conversation, turn, failed, notice = null) {
528
552
  if (turn.ended)
529
553
  return;
554
+ if (failed !== null)
555
+ logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
530
556
  if (conversation.turn === turn) {
531
557
  for (const event of conversation.running?.reader.closeTurn() ?? [])
532
558
  this.apply(conversation, turn, event);
@@ -540,7 +566,7 @@ export class Conversations {
540
566
  const body = Promise.allSettled(turn.uploads).then(() => ({
541
567
  parts: turn.parts.saved(),
542
568
  context: turn.parts.context,
543
- ...failed,
569
+ ...(failed === null ? {} : { failed: failed.failure }),
544
570
  }));
545
571
  void this.deliver(conversation, turn, 'ended', body).catch(logged);
546
572
  this.next(conversation);
@@ -550,20 +576,25 @@ export class Conversations {
550
576
  const running = conversation.running;
551
577
  const turn = this.begin(conversation);
552
578
  turn.prompted = true;
579
+ conversation.prompted = running.sessionId;
553
580
  running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
554
581
  const notice = answeredFailure(answer);
555
- const failed = notice ?? (answer.stopReason === 'refusal' ? REFUSED : null);
556
- this.end(conversation, turn, failed === null ? null : { failed }, notice);
582
+ if (notice !== null) {
583
+ this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
584
+ }
585
+ else {
586
+ this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
587
+ }
557
588
  }, async (error) => {
558
589
  if (running.adapter.connection.signal.aborted) {
559
- this.end(conversation, turn, { failed: running.killed ? KILLED : await running.adapter.exited });
590
+ this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
560
591
  }
561
592
  else {
562
593
  if (conversation.open === 1 && conversation.running === running) {
563
594
  running.scope.kill();
564
595
  this.detach(conversation, running);
565
596
  }
566
- this.end(conversation, turn, { failed: causeOf(error) });
597
+ this.end(conversation, turn, answeredWith(error));
567
598
  }
568
599
  });
569
600
  }
package/dist/copies.js CHANGED
@@ -36,6 +36,9 @@ function textOnly(paths) {
36
36
  const [named, them] = paths.length === 1 ? [`${paths[0]} is`, 'it'] : [`${paths.join(', ')} are`, 'them'];
37
37
  return `${named} not text, and house push sends text only; remove ${them} from the commit and place ${them} with house upload`;
38
38
  }
39
+ function outside(path) {
40
+ return { refused: true, text: `${path} is not a House Local copy; run house push inside one` };
41
+ }
39
42
  function short(commit) {
40
43
  return commit.slice(0, 12);
41
44
  }
@@ -63,6 +66,9 @@ function sidecar(path) {
63
66
  segments.splice(-1, 0, PROVENANCE);
64
67
  return segments.join('/');
65
68
  }
69
+ function recordOf(key) {
70
+ return join(kitHome(), 'local-copies', `${key}.json`);
71
+ }
66
72
  function rootOf(handle) {
67
73
  return `/rooms/${handle}`;
68
74
  }
@@ -209,7 +215,7 @@ export class LocalCopies {
209
215
  async mapping() {
210
216
  await this.answering;
211
217
  return [...this.copies.values()]
212
- .filter((copy) => copy.active)
218
+ .filter((copy) => copy.erasing === undefined)
213
219
  .map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
214
220
  .sort();
215
221
  }
@@ -232,25 +238,30 @@ export class LocalCopies {
232
238
  return this.holding([...new Set([...this.copies.keys(), ...synced.keys()])].sort(), async () => {
233
239
  const said = [];
234
240
  let refused = false;
235
- for (const copy of this.copies.values()) {
236
- if (synced.has(copy.key) || !copy.active)
241
+ for (const copy of [...this.copies.values()]) {
242
+ const returned = synced.has(copy.key);
243
+ if (returned && copy.erasing === undefined)
237
244
  continue;
238
- await this.save({ ...copy, active: false });
239
- said.push(`${rootOf(copy.room_handle)}: no longer synced to this Environment; its files stay`);
245
+ try {
246
+ await this.erase(copy);
247
+ said.push(`${rootOf(copy.room_handle)}: ${returned ? '' : 'no longer synced to this Environment; '}erased from the Local copy`);
248
+ }
249
+ catch (error) {
250
+ refused = true;
251
+ said.push(`${rootOf(copy.room_handle)}: ${causeOf(error)}`);
252
+ }
240
253
  }
241
254
  const current = [];
242
255
  for (const [key, handle] of synced) {
243
256
  try {
244
257
  const held = this.copies.get(key);
258
+ if (held?.erasing)
259
+ continue;
245
260
  if (held === undefined || !(await exists(held.repository))) {
246
261
  said.push(await this.materialize(key, handle));
247
262
  continue;
248
263
  }
249
- const copy = held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle });
250
- if (copy.active)
251
- current.push(copy);
252
- else
253
- said.push(...moved(copy, await this.save(await this.received({ ...copy, active: true }))));
264
+ current.push(held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle }));
254
265
  }
255
266
  catch (error) {
256
267
  refused = true;
@@ -276,6 +287,8 @@ export class LocalCopies {
276
287
  return;
277
288
  await this.exclusive(key, async () => {
278
289
  const copy = this.copies.get(key);
290
+ if (copy === undefined || copy.erasing)
291
+ return;
279
292
  let moves = copy.moves;
280
293
  for (const { from, to } of report.moves) {
281
294
  const held = moves.find((move) => move.worktree === report.worktree && move.to === from);
@@ -298,11 +311,11 @@ export class LocalCopies {
298
311
  async push(cwd, selector, caller) {
299
312
  const located = await run(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir', '--show-toplevel']);
300
313
  if (located.status !== 0)
301
- return { refused: true, text: `${cwd} is not a House Local copy; run house push inside one` };
314
+ return outside(cwd);
302
315
  const [common, worktree] = located.stdout.toString('utf8').split('\n');
303
316
  const key = await this.keyAt(dirname(common));
304
317
  if (key === undefined)
305
- return { refused: true, text: `${worktree} is not a House Local copy; run house push inside one` };
318
+ return outside(worktree);
306
319
  return this.exclusive(key, () => this.pushed(key, worktree, selector, caller));
307
320
  }
308
321
  async keyAt(repository) {
@@ -323,7 +336,7 @@ export class LocalCopies {
323
336
  return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
324
337
  }
325
338
  async save(copy) {
326
- const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
339
+ const path = recordOf(copy.key);
327
340
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
328
341
  const temporary = `${path}.${process.pid}`;
329
342
  await writeFile(temporary, JSON.stringify(copy), { mode: 0o600 });
@@ -331,6 +344,12 @@ export class LocalCopies {
331
344
  this.copies.set(copy.key, copy);
332
345
  return copy;
333
346
  }
347
+ async erase(copy) {
348
+ await this.save({ ...copy, erasing: true });
349
+ await rm(copy.repository, { recursive: true, force: true });
350
+ await rm(recordOf(copy.key), { force: true });
351
+ this.copies.delete(copy.key);
352
+ }
334
353
  async read(handle) {
335
354
  const prefixes = [rootOf(handle)];
336
355
  const discovery = await this.house.post('/kit/door/discover', { prefixes });
@@ -397,7 +416,6 @@ export class LocalCopies {
397
416
  room_ref: read.anchor,
398
417
  room_handle: handle,
399
418
  repository,
400
- active: true,
401
419
  house,
402
420
  position: read.position,
403
421
  log_epoch: read.log_epoch,
@@ -526,6 +544,8 @@ export class LocalCopies {
526
544
  }
527
545
  async pushed(key, worktree, selector, caller) {
528
546
  let copy = this.copies.get(key);
547
+ if (copy === undefined || copy.erasing)
548
+ return outside(worktree);
529
549
  if (copy.push?.state === 'submitted')
530
550
  return this.settle(copy, caller);
531
551
  const selected = await run(worktree, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${selector}^{commit}`]);
@@ -539,9 +559,6 @@ export class LocalCopies {
539
559
  }
540
560
  copy = await this.save(without(copy));
541
561
  }
542
- if (!copy.active) {
543
- return { refused: true, text: `${rootOf(copy.room_handle)} is no longer synced to this Environment, so House takes no push from it` };
544
- }
545
562
  const listed = (await git(copy.repository, ['rev-list', '--topo-order', commit])).split('\n');
546
563
  const base = listed.find((ancestor) => ancestor in copy.manifests);
547
564
  if (base === undefined) {
package/dist/events.js CHANGED
@@ -513,7 +513,8 @@ class GrokReader extends Reader {
513
513
  }
514
514
  }
515
515
  export function answeredFailure(answer) {
516
- return answer._meta?.jetbrains?.air?.sessionFailure?.title ?? null;
516
+ const failure = answer._meta?.jetbrains?.air?.sessionFailure;
517
+ return typeof failure?.title === 'string' ? { title: failure.title, limit: failure.category === 'limit' } : null;
517
518
  }
518
519
  export function readerFor(kind) {
519
520
  if (kind === 'claude-agent-acp')
@@ -4,8 +4,9 @@ const SOURCE_LINE = "From House:";
4
4
  const HOUSE_LINE = "Work with House through the `house` CLI: run `house --help`, and `house find_command` for House's guides and formats.";
5
5
  const FILE_LINE = 'To give the User a file, upload it with `house upload` and link it; for an App, a Site or a website, follow `house find_command \'{"query":"app"}\'`. Never start a server.';
6
6
  const READ_LINE = "Optional per-Room instructions: `/rooms/private/rooms/<handle>/AGENTS.md`.";
7
+ const WAIT_LINE = 'To get what another Agent will write into a Room, `create_trigger` on where it will land with a `conversation` action, then end your turn: this conversation continues when it lands.';
7
8
  const COPIES_LINE = `House Local copies; before work run \`house fetch\` and rebase onto \`${HOUSE_REF}\`; after it commit, then \`house push\`:`;
8
9
  export async function instructions(bridge, base, copies) {
9
- const lines = [SOURCE_LINE, HOUSE_LINE, FILE_LINE, READ_LINE, ...(copies.length === 0 ? [] : [COPIES_LINE, ...copies])];
10
+ const lines = [SOURCE_LINE, HOUSE_LINE, FILE_LINE, READ_LINE, WAIT_LINE, ...(copies.length === 0 ? [] : [COPIES_LINE, ...copies])];
10
11
  return [lines.join('\n'), base, (await readHowWeWork(bridge)).replace(/\n$/, '')].filter((part) => part !== '').join('\n\n');
11
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.2.1-c64c31420c9bf",
3
+ "version": "0.2.1-c70284f7a7466",
4
4
  "description": "House Kit connects an Environment to agents.house.",
5
5
  "license": "MIT",
6
6
  "repository": {