@agentshouse/kit 0.2.1-c5262a8cc063d → 0.2.1-c585225098c15
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 +42 -41
- package/dist/acp.js +97 -107
- package/dist/agents.js +180 -51
- package/dist/api.js +36 -3
- package/dist/app-git.js +48 -0
- package/dist/authority-main.js +2 -2
- package/dist/bridge.js +40 -59
- package/dist/building.js +46 -0
- package/dist/calls.js +71 -0
- package/dist/chatgpt.js +32 -0
- package/dist/clis-main.js +2 -2
- package/dist/clis.js +171 -76
- package/dist/codex-main.js +58 -0
- package/dist/connection.js +116 -0
- package/dist/conversations.js +696 -324
- package/dist/copies.js +205 -150
- package/dist/end.js +27 -0
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +525 -0
- package/dist/files.js +140 -17
- package/dist/git.js +13 -11
- package/dist/home.js +96 -7
- package/dist/house-cli.js +299 -0
- package/dist/house-main.js +7 -159
- package/dist/instructions.js +8 -10
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -19
- package/dist/login.js +119 -104
- package/dist/logout.js +14 -13
- package/dist/mcp.js +8 -3
- package/dist/parts.js +82 -0
- package/dist/placement.js +39 -0
- package/dist/refusals.js +12 -0
- package/dist/relay.js +41 -0
- package/dist/resident.js +118 -51
- package/dist/scope.js +206 -0
- package/dist/scripts.js +255 -0
- package/dist/secret-input.js +4 -1
- package/dist/service-main.js +9 -0
- package/dist/service.js +55 -0
- package/dist/shell.js +3 -0
- package/dist/sign-in.js +40 -32
- package/dist/silence.js +28 -0
- package/dist/skills.js +49 -35
- package/dist/transcription.js +129 -41
- package/dist/translate.js +2 -8
- package/dist/update.js +117 -53
- package/dist/wire.js +22 -0
- package/dist/work.js +346 -0
- package/package.json +20 -20
- package/dist/blocks.js +0 -51
- package/dist/bootstrap.json +0 -24
- package/dist/stream.js +0 -46
- package/how-we-work.md +0 -23
- package/skills/ask-matt/LICENSE +0 -21
- package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
- package/skills/ask-matt/SKILL.md +0 -91
- package/skills/ask-matt/agents/openai.yaml +0 -4
- package/skills/code-review/LICENSE +0 -21
- package/skills/code-review/SKILL.md +0 -89
- package/skills/code-review/agents/openai.yaml +0 -3
- package/skills/codebase-design/DEEPENING.md +0 -37
- package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
- package/skills/codebase-design/LICENSE +0 -21
- package/skills/codebase-design/SKILL.md +0 -116
- package/skills/codebase-design/agents/openai.yaml +0 -3
- package/skills/diagnosing-bugs/LICENSE +0 -21
- package/skills/diagnosing-bugs/SKILL.md +0 -140
- package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
- package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
- package/skills/domain-modeling/ADR-FORMAT.md +0 -47
- package/skills/domain-modeling/CONTEXT-FORMAT.md +0 -60
- package/skills/domain-modeling/LICENSE +0 -21
- package/skills/domain-modeling/SKILL.md +0 -76
- package/skills/domain-modeling/agents/openai.yaml +0 -3
- package/skills/grill-me/LICENSE +0 -21
- package/skills/grill-me/SKILL.md +0 -8
- package/skills/grill-me/agents/openai.yaml +0 -4
- package/skills/grill-with-docs/LICENSE +0 -21
- package/skills/grill-with-docs/SKILL.md +0 -8
- package/skills/grill-with-docs/agents/openai.yaml +0 -4
- package/skills/grilling/LICENSE +0 -21
- package/skills/grilling/SKILL.md +0 -30
- package/skills/grilling/agents/openai.yaml +0 -3
- package/skills/handoff/LICENSE +0 -21
- package/skills/handoff/SKILL.md +0 -17
- package/skills/handoff/agents/openai.yaml +0 -4
- package/skills/implement/LICENSE +0 -21
- package/skills/implement/SKILL.md +0 -16
- package/skills/implement/agents/openai.yaml +0 -4
- package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
- package/skills/improve-codebase-architecture/LICENSE +0 -21
- package/skills/improve-codebase-architecture/SKILL.md +0 -72
- package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
- package/skills/prototype/LICENSE +0 -21
- package/skills/prototype/LOGIC.md +0 -67
- package/skills/prototype/SKILL.md +0 -28
- package/skills/prototype/UI.md +0 -112
- package/skills/prototype/agents/openai.yaml +0 -3
- package/skills/research/LICENSE +0 -21
- package/skills/research/SKILL.md +0 -14
- package/skills/research/agents/openai.yaml +0 -3
- package/skills/resolving-merge-conflicts/LICENSE +0 -21
- package/skills/resolving-merge-conflicts/SKILL.md +0 -16
- package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
- package/skills/tdd/LICENSE +0 -21
- package/skills/tdd/SKILL.md +0 -40
- package/skills/tdd/agents/openai.yaml +0 -3
- package/skills/tdd/mocking.md +0 -59
- package/skills/tdd/tests.md +0 -77
- package/skills/teach/GLOSSARY-FORMAT.md +0 -35
- package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
- package/skills/teach/LICENSE +0 -21
- package/skills/teach/MISSION-FORMAT.md +0 -31
- package/skills/teach/RESOURCES-FORMAT.md +0 -32
- package/skills/teach/SKILL.md +0 -141
- package/skills/teach/agents/openai.yaml +0 -4
- package/skills/to-questionnaire/LICENSE +0 -21
- package/skills/to-questionnaire/SKILL.md +0 -55
- package/skills/to-questionnaire/agents/openai.yaml +0 -4
- package/skills/to-spec/LICENSE +0 -21
- package/skills/to-spec/SKILL.md +0 -76
- package/skills/to-spec/agents/openai.yaml +0 -4
- package/skills/to-tickets/LICENSE +0 -21
- package/skills/to-tickets/SKILL.md +0 -106
- package/skills/to-tickets/agents/openai.yaml +0 -4
- package/skills/wait-what/LICENSE +0 -21
- package/skills/wait-what/SKILL.md +0 -8
- package/skills/wait-what/agents/openai.yaml +0 -4
- package/skills/wayfinder/LICENSE +0 -21
- package/skills/wayfinder/SKILL.md +0 -129
- package/skills/wayfinder/agents/openai.yaml +0 -4
- package/skills/writing-for-agents/LICENSE +0 -21
- package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
- package/skills/writing-for-agents/SKILL.md +0 -83
- package/skills/writing-for-agents/agents/openai.yaml +0 -3
package/dist/conversations.js
CHANGED
|
@@ -1,56 +1,147 @@
|
|
|
1
|
-
import { client, } from '@agentclientprotocol/sdk';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
1
|
+
import { client, RequestError, } from '@agentclientprotocol/sdk';
|
|
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
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { startAdapter } from './acp.js';
|
|
7
|
+
import { finalRefusal, HouseRefusal } from './api.js';
|
|
8
8
|
import { openBridge } from './bridge.js';
|
|
9
9
|
import { CLIS } from './clis.js';
|
|
10
|
-
import {
|
|
10
|
+
import { answeredFailure, readerFor } from './events.js';
|
|
11
|
+
import { inspectedFile, linkedPaths, storeRefusal, uploaded } from './files.js';
|
|
11
12
|
import { agentBase } from './home.js';
|
|
12
13
|
import { instructions } from './instructions.js';
|
|
14
|
+
import { Parts } from './parts.js';
|
|
15
|
+
import { refusalOf } from './refusals.js';
|
|
16
|
+
import { relayed } from './relay.js';
|
|
17
|
+
import { killMarked, Scope } from './scope.js';
|
|
13
18
|
import { holdSecretInput } from './secret-input.js';
|
|
14
|
-
import { createHowWeWork } from './skills.js';
|
|
15
19
|
import { transcribe } from './transcription.js';
|
|
20
|
+
import { workFor } from './work.js';
|
|
21
|
+
const SIGNED_OUT = RequestError.authRequired().code;
|
|
16
22
|
const KILLED = 'the conversation was killed';
|
|
23
|
+
const INTERRUPTED = 'the conversation was interrupted';
|
|
24
|
+
const REFUSED = 'the CLI refused the prompt';
|
|
25
|
+
const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
|
|
26
|
+
const GONE = 'House no longer holds this conversation; start a new one';
|
|
27
|
+
const RETIRED = "this conversation's Agent is retired, so it cannot start";
|
|
28
|
+
const ENDED = {
|
|
29
|
+
conversation_not_found: GONE,
|
|
30
|
+
conversation_historical: RETIRED,
|
|
31
|
+
};
|
|
32
|
+
const NO_START = "the Agent's CLI did not start";
|
|
33
|
+
const NO_SESSION = "the Agent's CLI did not open a session";
|
|
34
|
+
const NO_RESUME = "the Agent's CLI could not resume this conversation";
|
|
35
|
+
const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
|
|
36
|
+
const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
|
|
37
|
+
const UNSTEERED = "the Agent's CLI did not take the message into its running turn";
|
|
38
|
+
class NotStarted extends Error {
|
|
39
|
+
}
|
|
40
|
+
class ResumeRefused extends NotStarted {
|
|
41
|
+
}
|
|
42
|
+
async function step(reason, work) {
|
|
43
|
+
try {
|
|
44
|
+
return await work;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function signedOut(error) {
|
|
51
|
+
return typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
|
|
52
|
+
}
|
|
53
|
+
function unopened(reason) {
|
|
54
|
+
return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
|
|
55
|
+
}
|
|
56
|
+
// A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
|
|
57
|
+
const STARTING_SHOWN_AFTER_MS = 1_000;
|
|
58
|
+
function unbridged(error) {
|
|
59
|
+
return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
|
|
60
|
+
}
|
|
61
|
+
const EXIT_PLAN_MODE = {
|
|
62
|
+
parse(value) {
|
|
63
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
64
|
+
throw new Error('_x.ai/exit_plan_mode needs an object');
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'done', 'option', 'answer']);
|
|
17
70
|
function causeOf(error) {
|
|
18
71
|
return error instanceof Error ? error.message : String(error);
|
|
19
72
|
}
|
|
73
|
+
function answeredWith(error) {
|
|
74
|
+
return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
|
|
75
|
+
}
|
|
76
|
+
function exitedWith(running, cause) {
|
|
77
|
+
if (running.killed)
|
|
78
|
+
return { failure: 'killed', cause: KILLED };
|
|
79
|
+
return { failure: running.adapter.silent() ? 'silent' : 'exited', cause };
|
|
80
|
+
}
|
|
20
81
|
function answerMessage(request, response) {
|
|
21
82
|
return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
|
|
22
83
|
}
|
|
84
|
+
async function sentPart(url, bytes, headers) {
|
|
85
|
+
const answer = await fetch(relayed(url), { method: 'PUT', headers, body: bytes });
|
|
86
|
+
await answer.arrayBuffer();
|
|
87
|
+
if (!answer.ok)
|
|
88
|
+
throw storeRefusal('upload', answer.status);
|
|
89
|
+
}
|
|
23
90
|
function logged(error) {
|
|
24
91
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
25
92
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
93
|
+
function refusal(conversation, error) {
|
|
94
|
+
if (conversation.kills > 0)
|
|
95
|
+
return KILLED;
|
|
96
|
+
const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
|
|
97
|
+
logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
|
|
98
|
+
return causeOf(error);
|
|
99
|
+
}
|
|
100
|
+
function audioOf(result) {
|
|
101
|
+
if (result.isError === true)
|
|
102
|
+
return null;
|
|
103
|
+
const file = inspectedFile(result.content.map((content) => content.text).join('\n'));
|
|
104
|
+
return file?.media_type?.startsWith('audio/') === true ? file : null;
|
|
105
|
+
}
|
|
106
|
+
function marker(bridge) {
|
|
107
|
+
return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
|
|
108
|
+
}
|
|
109
|
+
function steers(adapter) {
|
|
110
|
+
return adapter.initialized._meta?.steering?.supported === true;
|
|
28
111
|
}
|
|
29
112
|
class Turn {
|
|
30
113
|
id = randomUUID();
|
|
31
|
-
|
|
32
|
-
segment = null;
|
|
33
|
-
admitted = false;
|
|
34
|
-
notes = [];
|
|
35
|
-
sent = [];
|
|
36
|
-
unsentFrom = null;
|
|
37
|
-
draftSequence = 0;
|
|
38
|
-
planSequence = 0;
|
|
39
|
-
entries = null;
|
|
114
|
+
parts;
|
|
40
115
|
questions = 0;
|
|
41
116
|
secrets = 0;
|
|
42
|
-
|
|
117
|
+
prompted = false;
|
|
118
|
+
taken = null;
|
|
43
119
|
ended = false;
|
|
120
|
+
images = 0;
|
|
121
|
+
uploads = [];
|
|
122
|
+
constructor(emit) {
|
|
123
|
+
this.parts = new Parts(emit);
|
|
124
|
+
}
|
|
44
125
|
}
|
|
45
126
|
class Conversation {
|
|
46
127
|
id;
|
|
47
128
|
running = null;
|
|
129
|
+
session = null;
|
|
48
130
|
turn = null;
|
|
49
131
|
commands = null;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
132
|
+
options = null;
|
|
133
|
+
exitUntold = false;
|
|
134
|
+
context = null;
|
|
135
|
+
leftovers = new Set();
|
|
136
|
+
waiting = [];
|
|
137
|
+
done = false;
|
|
138
|
+
sending = null;
|
|
139
|
+
subagents = new Map();
|
|
140
|
+
held = [];
|
|
53
141
|
opening = null;
|
|
142
|
+
bridge = null;
|
|
143
|
+
launch = null;
|
|
144
|
+
prompted = null;
|
|
54
145
|
kills = 0;
|
|
55
146
|
killed = new AbortController();
|
|
56
147
|
interrupts = 0;
|
|
@@ -58,6 +149,11 @@ class Conversation {
|
|
|
58
149
|
open = 0;
|
|
59
150
|
queue = Promise.resolve();
|
|
60
151
|
reports = Promise.resolve();
|
|
152
|
+
starting = null;
|
|
153
|
+
preparing = null;
|
|
154
|
+
told = null;
|
|
155
|
+
untold = false;
|
|
156
|
+
shown = null;
|
|
61
157
|
constructor(id) {
|
|
62
158
|
this.id = id;
|
|
63
159
|
}
|
|
@@ -73,55 +169,80 @@ function asksSecret(request) {
|
|
|
73
169
|
export class Conversations {
|
|
74
170
|
kit;
|
|
75
171
|
conversations = new Map();
|
|
76
|
-
acks = new Map();
|
|
77
172
|
questions = new Map();
|
|
78
|
-
carrying = 0;
|
|
79
173
|
reporting = 0;
|
|
174
|
+
watched = false;
|
|
80
175
|
constructor(kit) {
|
|
81
176
|
this.kit = kit;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
})();
|
|
102
|
-
carried
|
|
103
|
-
.then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
|
|
104
|
-
.catch(logged)
|
|
105
|
-
.finally(() => {
|
|
106
|
-
this.carrying--;
|
|
107
|
-
this.kit.changed();
|
|
177
|
+
// A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
|
|
178
|
+
setInterval(() => this.look(), 2_000);
|
|
179
|
+
}
|
|
180
|
+
call(call) {
|
|
181
|
+
void this.kit.calls.carry(call.input_id, () => {
|
|
182
|
+
const conversation = this.conversation(call.conversation_id);
|
|
183
|
+
if (call.kind === 'kill') {
|
|
184
|
+
conversation.kills++;
|
|
185
|
+
conversation.killed.abort(new Error(KILLED));
|
|
186
|
+
for (const waiting of conversation.waiting.splice(0))
|
|
187
|
+
waiting.settle({ refused: KILLED });
|
|
188
|
+
this.halt(conversation);
|
|
189
|
+
}
|
|
190
|
+
if (call.kind === 'interrupt') {
|
|
191
|
+
conversation.interrupts++;
|
|
192
|
+
conversation.interrupted.abort();
|
|
193
|
+
}
|
|
194
|
+
const handled = conversation.queue.then(() => this.carry(conversation, call));
|
|
195
|
+
conversation.queue = handled.catch(() => undefined);
|
|
196
|
+
return handled.then(async ({ result }) => ({ result: await result }));
|
|
108
197
|
});
|
|
109
198
|
}
|
|
110
199
|
idle() {
|
|
111
|
-
if (this.
|
|
200
|
+
if (this.reporting > 0)
|
|
112
201
|
return false;
|
|
113
202
|
for (const conversation of this.conversations.values()) {
|
|
114
203
|
const turn = conversation.turn;
|
|
115
204
|
const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
|
|
116
|
-
if (conversation.
|
|
205
|
+
if (conversation.open > (asking ? 1 : 0))
|
|
117
206
|
return false;
|
|
118
207
|
}
|
|
119
|
-
|
|
208
|
+
const conversations = [...this.conversations.values()];
|
|
209
|
+
if (conversations.some((conversation) => conversation.running?.work.busy()))
|
|
210
|
+
return false;
|
|
211
|
+
return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
|
|
212
|
+
}
|
|
213
|
+
quiet() {
|
|
214
|
+
return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
|
|
215
|
+
}
|
|
216
|
+
look() {
|
|
217
|
+
for (const conversation of this.conversations.values()) {
|
|
218
|
+
for (const scope of conversation.leftovers) {
|
|
219
|
+
if (!scope.empty())
|
|
220
|
+
continue;
|
|
221
|
+
scope.close();
|
|
222
|
+
conversation.leftovers.delete(scope);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
this.kit.changed();
|
|
120
226
|
}
|
|
121
227
|
opened() {
|
|
228
|
+
this.watched = false;
|
|
229
|
+
for (const conversation of this.conversations.values()) {
|
|
230
|
+
if (conversation.running !== null)
|
|
231
|
+
this.facts(conversation);
|
|
232
|
+
else if (conversation.exitUntold)
|
|
233
|
+
this.process(conversation, false);
|
|
234
|
+
if (conversation.untold || conversation.told !== null)
|
|
235
|
+
this.retell(conversation);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
watching(watched) {
|
|
239
|
+
const resend = watched && !this.watched;
|
|
240
|
+
this.watched = watched;
|
|
241
|
+
if (!resend)
|
|
242
|
+
return;
|
|
122
243
|
for (const conversation of this.conversations.values()) {
|
|
123
244
|
if (conversation.turn !== null)
|
|
124
|
-
conversation.turn
|
|
245
|
+
this.replay(conversation, conversation.turn);
|
|
125
246
|
}
|
|
126
247
|
}
|
|
127
248
|
conversation(id) {
|
|
@@ -132,93 +253,309 @@ export class Conversations {
|
|
|
132
253
|
}
|
|
133
254
|
return conversation;
|
|
134
255
|
}
|
|
135
|
-
async carry(conversation,
|
|
136
|
-
if (
|
|
137
|
-
return this.open(conversation,
|
|
138
|
-
if (
|
|
139
|
-
return this.message(conversation,
|
|
140
|
-
if (
|
|
256
|
+
async carry(conversation, call) {
|
|
257
|
+
if (call.kind === 'open')
|
|
258
|
+
return this.open(conversation, call);
|
|
259
|
+
if (call.kind === 'message')
|
|
260
|
+
return this.message(conversation, call);
|
|
261
|
+
if (call.kind === 'done') {
|
|
262
|
+
conversation.done = true;
|
|
263
|
+
this.next(conversation);
|
|
264
|
+
return { result: Promise.resolve({}) };
|
|
265
|
+
}
|
|
266
|
+
await this.launched(conversation);
|
|
267
|
+
if (call.kind === 'interrupt') {
|
|
141
268
|
if (--conversation.interrupts === 0)
|
|
142
269
|
conversation.interrupted = new AbortController();
|
|
143
|
-
await this.interrupt(conversation, String(
|
|
270
|
+
await this.interrupt(conversation, String(call.turn_id));
|
|
144
271
|
}
|
|
145
|
-
if (
|
|
272
|
+
if (call.kind === 'kill') {
|
|
146
273
|
await this.kill(conversation);
|
|
274
|
+
await conversation.sending;
|
|
147
275
|
if (--conversation.kills === 0)
|
|
148
276
|
conversation.killed = new AbortController();
|
|
149
277
|
}
|
|
150
|
-
if (
|
|
151
|
-
await this.option(conversation, String(
|
|
152
|
-
if (
|
|
153
|
-
const question = this.questions.get(String(
|
|
278
|
+
if (call.kind === 'option')
|
|
279
|
+
await this.option(conversation, String(call.option), call.value);
|
|
280
|
+
if (call.kind === 'answer') {
|
|
281
|
+
const question = this.questions.get(String(call.interaction_id));
|
|
154
282
|
if (question === undefined) {
|
|
155
|
-
|
|
283
|
+
const text = answerMessage(call.request, call.response);
|
|
284
|
+
return this.message(conversation, { ...call, text, first: false, message_id: null });
|
|
156
285
|
}
|
|
157
|
-
question.answer(
|
|
286
|
+
question.answer(call.response);
|
|
158
287
|
}
|
|
159
|
-
return {};
|
|
288
|
+
return { result: Promise.resolve({}) };
|
|
289
|
+
}
|
|
290
|
+
async open(conversation, call) {
|
|
291
|
+
await this.stop(conversation);
|
|
292
|
+
const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
|
|
293
|
+
return {
|
|
294
|
+
result: launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) })),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
async stop(conversation) {
|
|
298
|
+
const running = conversation.running;
|
|
299
|
+
running?.scope.kill();
|
|
300
|
+
await running?.adapter.exited;
|
|
301
|
+
}
|
|
302
|
+
launch(conversation, route, sessionId, empty) {
|
|
303
|
+
let opened;
|
|
304
|
+
let failed;
|
|
305
|
+
const bridge = new Promise((resolve, reject) => {
|
|
306
|
+
opened = resolve;
|
|
307
|
+
failed = reject;
|
|
308
|
+
});
|
|
309
|
+
bridge.catch(() => undefined);
|
|
310
|
+
const session = this.start(conversation, route, sessionId, empty, opened);
|
|
311
|
+
session.catch(failed);
|
|
312
|
+
const launch = { bridge, session };
|
|
313
|
+
conversation.launch = launch;
|
|
314
|
+
void session
|
|
315
|
+
.catch(() => undefined)
|
|
316
|
+
.then(() => {
|
|
317
|
+
if (conversation.launch !== launch)
|
|
318
|
+
return;
|
|
319
|
+
conversation.launch = null;
|
|
320
|
+
this.closeIdleBridge(conversation);
|
|
321
|
+
this.next(conversation);
|
|
322
|
+
});
|
|
323
|
+
this.next(conversation);
|
|
324
|
+
return launch;
|
|
325
|
+
}
|
|
326
|
+
async launched(conversation) {
|
|
327
|
+
while (conversation.launch !== null)
|
|
328
|
+
await conversation.launch.session.catch(() => undefined);
|
|
329
|
+
}
|
|
330
|
+
instructed(conversation, route) {
|
|
331
|
+
const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
|
|
332
|
+
if (bridge === undefined)
|
|
333
|
+
return null;
|
|
334
|
+
const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
|
|
335
|
+
read.catch(() => undefined);
|
|
336
|
+
return read;
|
|
337
|
+
}
|
|
338
|
+
async message(conversation, call) {
|
|
339
|
+
const content = [
|
|
340
|
+
...(typeof call.recent === 'string' ? [{ type: 'text', text: call.recent }] : []),
|
|
341
|
+
{ type: 'text', text: String(call.text) },
|
|
342
|
+
];
|
|
343
|
+
let route;
|
|
344
|
+
let instructing = null;
|
|
345
|
+
try {
|
|
346
|
+
route = await this.route(conversation, call.agent_id);
|
|
347
|
+
if (call.first === true)
|
|
348
|
+
instructing = this.instructed(conversation, route);
|
|
349
|
+
if (call.message_id !== null)
|
|
350
|
+
content.push(...(await this.transcribed(conversation, String(call.message_id), String(call.text))));
|
|
351
|
+
if (conversation.kills > 0)
|
|
352
|
+
throw new Error(KILLED);
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
this.closeIdleBridge(conversation);
|
|
356
|
+
this.prepares(conversation, null);
|
|
357
|
+
return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
|
|
358
|
+
}
|
|
359
|
+
conversation.preparing = null;
|
|
360
|
+
const { promise, resolve: settle } = Promise.withResolvers();
|
|
361
|
+
const waiting = { call, route, content, instructing, settle };
|
|
362
|
+
if (call.at_once === true && conversation.open > 0)
|
|
363
|
+
await this.atOnce(conversation, waiting);
|
|
364
|
+
else
|
|
365
|
+
conversation.waiting.push(waiting);
|
|
366
|
+
this.next(conversation);
|
|
367
|
+
return { result: promise };
|
|
368
|
+
}
|
|
369
|
+
async atOnce(conversation, waiting) {
|
|
370
|
+
while (conversation.sending !== null)
|
|
371
|
+
await conversation.sending;
|
|
372
|
+
const running = conversation.running;
|
|
373
|
+
if (running !== null && conversation.open > 0 && steers(running.adapter)) {
|
|
374
|
+
conversation.sending = this.steer(conversation, running, waiting);
|
|
375
|
+
await conversation.sending;
|
|
376
|
+
conversation.sending = null;
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
this.ahead(conversation, waiting);
|
|
380
|
+
if (conversation.turn !== null)
|
|
381
|
+
await this.interrupt(conversation, conversation.turn.id);
|
|
160
382
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
383
|
+
ahead(conversation, waiting) {
|
|
384
|
+
const behind = conversation.waiting.findIndex((held) => held.call.at_once !== true);
|
|
385
|
+
conversation.waiting.splice(behind === -1 ? conversation.waiting.length : behind, 0, waiting);
|
|
386
|
+
}
|
|
387
|
+
async steer(conversation, running, waiting) {
|
|
388
|
+
let outcome;
|
|
164
389
|
try {
|
|
165
|
-
|
|
390
|
+
({ outcome } = await running.adapter.connection.agent.request('_session/steering', {
|
|
391
|
+
sessionId: running.sessionId,
|
|
392
|
+
prompt: waiting.content,
|
|
393
|
+
_meta: { steering: { idleBehavior: 'promptRequired' } },
|
|
394
|
+
}));
|
|
166
395
|
}
|
|
167
396
|
catch (error) {
|
|
168
|
-
|
|
397
|
+
outcome = error;
|
|
398
|
+
}
|
|
399
|
+
if (outcome === 'promptRequired') {
|
|
400
|
+
this.ahead(conversation, waiting);
|
|
401
|
+
}
|
|
402
|
+
else if (outcome === 'injected' || outcome === 'startedNewTurn') {
|
|
403
|
+
this.kit.calls.keepResult(String(waiting.call.input_id), {});
|
|
404
|
+
waiting.settle({});
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
const cause = outcome instanceof Error ? outcome : new Error(`the CLI answered ${JSON.stringify(outcome)}`);
|
|
408
|
+
waiting.settle({ refused: refusal(conversation, new NotStarted(UNSTEERED, { cause })) });
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
next(conversation) {
|
|
412
|
+
if (conversation.sending !== null || conversation.open > 0)
|
|
413
|
+
return;
|
|
414
|
+
const waiting = conversation.waiting.shift();
|
|
415
|
+
if (waiting === undefined) {
|
|
416
|
+
const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
|
|
417
|
+
if (conversation.starting !== step)
|
|
418
|
+
this.starts(conversation, step);
|
|
419
|
+
if (conversation.done && conversation.launch === null)
|
|
420
|
+
void this.stop(conversation).catch(logged);
|
|
421
|
+
return;
|
|
169
422
|
}
|
|
423
|
+
conversation.sending = this.send(conversation, waiting).finally(() => {
|
|
424
|
+
conversation.sending = null;
|
|
425
|
+
this.next(conversation);
|
|
426
|
+
});
|
|
170
427
|
}
|
|
171
|
-
async
|
|
172
|
-
|
|
428
|
+
async send(conversation, waiting) {
|
|
429
|
+
const { call } = waiting;
|
|
430
|
+
let result = {};
|
|
431
|
+
let started = false;
|
|
432
|
+
let instructing = waiting.instructing;
|
|
173
433
|
const prompt = [];
|
|
174
434
|
try {
|
|
175
|
-
const route = await this.route(conversation, input.agent_id);
|
|
176
|
-
const files = input.files;
|
|
177
|
-
const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
|
|
178
|
-
const lines = await this.transcribed(conversation, files, paths);
|
|
179
435
|
if (conversation.running === null) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
436
|
+
this.starts(conversation, 'session');
|
|
437
|
+
await this.launched(conversation);
|
|
438
|
+
}
|
|
439
|
+
if (conversation.running === null) {
|
|
440
|
+
const stored = conversation.session ?? call.provider_session_id;
|
|
441
|
+
const empty = call.first === true && conversation.prompted !== stored;
|
|
442
|
+
const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
|
|
443
|
+
if (call.first === true)
|
|
444
|
+
instructing = this.instructed(conversation, waiting.route);
|
|
445
|
+
started = (await launch.session) !== stored;
|
|
446
|
+
}
|
|
447
|
+
const session = conversation.running.sessionId;
|
|
448
|
+
if (started || (call.first === true && session !== call.provider_session_id)) {
|
|
449
|
+
result = { provider_session_id: session };
|
|
183
450
|
}
|
|
184
|
-
if (
|
|
185
|
-
prompt.push({
|
|
186
|
-
type: 'text',
|
|
187
|
-
text: await instructions(conversation.running.bridge, route.base_instructions, await this.kit.copies.mapping()),
|
|
188
|
-
});
|
|
451
|
+
if (call.first === true) {
|
|
452
|
+
prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
|
|
189
453
|
}
|
|
190
|
-
prompt.push({ type: 'text', text: String(input.text) });
|
|
191
|
-
if (lines.length > 0)
|
|
192
|
-
prompt.push({ type: 'text', text: lines.join('\n') });
|
|
193
454
|
if (conversation.kills > 0)
|
|
194
455
|
throw new Error(KILLED);
|
|
195
456
|
}
|
|
196
457
|
catch (error) {
|
|
197
|
-
|
|
458
|
+
this.starts(conversation, null);
|
|
459
|
+
if (started) {
|
|
460
|
+
conversation.session = null;
|
|
198
461
|
await this.kill(conversation);
|
|
199
|
-
|
|
462
|
+
}
|
|
463
|
+
const refused = refusal(conversation, error);
|
|
464
|
+
const resumeRefused = call.kind === 'message' && conversation.kills === 0 && error instanceof ResumeRefused;
|
|
465
|
+
waiting.settle(resumeRefused ? { refused, resume_refused: true } : { refused });
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
this.starts(conversation, null);
|
|
469
|
+
this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
|
|
470
|
+
waiting.settle(result);
|
|
471
|
+
}
|
|
472
|
+
starts(conversation, step) {
|
|
473
|
+
conversation.starting = step;
|
|
474
|
+
if (conversation.shown !== null)
|
|
475
|
+
clearTimeout(conversation.shown);
|
|
476
|
+
conversation.shown = null;
|
|
477
|
+
if (step === null || conversation.told !== null) {
|
|
478
|
+
this.tell(conversation, step);
|
|
479
|
+
return;
|
|
200
480
|
}
|
|
201
|
-
|
|
202
|
-
|
|
481
|
+
conversation.shown = setTimeout(() => {
|
|
482
|
+
conversation.shown = null;
|
|
483
|
+
if (conversation.starting === step)
|
|
484
|
+
this.tell(conversation, step);
|
|
485
|
+
}, STARTING_SHOWN_AFTER_MS);
|
|
203
486
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
487
|
+
prepares(conversation, step) {
|
|
488
|
+
conversation.preparing = step;
|
|
489
|
+
this.next(conversation);
|
|
490
|
+
}
|
|
491
|
+
tell(conversation, step) {
|
|
492
|
+
if (conversation.told === step && !conversation.untold)
|
|
493
|
+
return;
|
|
494
|
+
conversation.told = step;
|
|
495
|
+
this.retell(conversation);
|
|
496
|
+
}
|
|
497
|
+
retell(conversation) {
|
|
498
|
+
conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
|
|
499
|
+
}
|
|
500
|
+
async transcribed(conversation, messageId, text) {
|
|
501
|
+
const paths = linkedPaths(text);
|
|
502
|
+
if (paths.length === 0)
|
|
503
|
+
return [];
|
|
504
|
+
const killed = conversation.killed.signal;
|
|
505
|
+
const interrupted = conversation.interrupted.signal;
|
|
506
|
+
const transcribing = AbortSignal.any([killed, interrupted]);
|
|
507
|
+
let signal = killed;
|
|
508
|
+
const blocks = [];
|
|
509
|
+
try {
|
|
510
|
+
const bridge = await step(unbridged, unlessAborted(this.bridged(conversation), killed));
|
|
511
|
+
const read = async (path) => audioOf(await step(unbridged, unlessAborted(bridge.tool('inspect', { path }), signal)));
|
|
512
|
+
for (const path of paths) {
|
|
513
|
+
let answered = await read(path);
|
|
514
|
+
if (answered === null)
|
|
515
|
+
continue;
|
|
516
|
+
signal = transcribing;
|
|
517
|
+
this.prepares(conversation, 'transcription');
|
|
518
|
+
const said = await transcribe(async () => {
|
|
519
|
+
const address = answered ?? (await read(path));
|
|
520
|
+
answered = null;
|
|
521
|
+
if (address === null)
|
|
522
|
+
throw new Error(`${path} is no longer an audio file`);
|
|
523
|
+
return address;
|
|
524
|
+
}, signal);
|
|
525
|
+
const transcript = `/kit/conversations/${conversation.id}/messages/${messageId}/transcripts`;
|
|
526
|
+
await step(unbridged, unlessAborted(this.kit.house.deliver(transcript, { path, text: said }, () => !signal.aborted), signal));
|
|
527
|
+
blocks.push({ type: 'text', text: `${path}\nTranscript: ${said}` });
|
|
219
528
|
}
|
|
220
529
|
}
|
|
221
|
-
|
|
530
|
+
catch (error) {
|
|
531
|
+
if (signal === transcribing && interrupted.aborted && conversation.kills === 0)
|
|
532
|
+
throw new NotStarted(INTERRUPTED, { cause: error });
|
|
533
|
+
throw error;
|
|
534
|
+
}
|
|
535
|
+
return blocks;
|
|
536
|
+
}
|
|
537
|
+
bridged(conversation) {
|
|
538
|
+
if (conversation.bridge === null) {
|
|
539
|
+
const bridging = openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
|
|
540
|
+
conversation.bridge = bridging;
|
|
541
|
+
bridging.catch(() => {
|
|
542
|
+
if (conversation.bridge === bridging)
|
|
543
|
+
conversation.bridge = null;
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
return conversation.bridge;
|
|
547
|
+
}
|
|
548
|
+
closeBridge(conversation, bridging, bridge) {
|
|
549
|
+
bridge.close();
|
|
550
|
+
if (conversation.bridge === bridging)
|
|
551
|
+
conversation.bridge = null;
|
|
552
|
+
}
|
|
553
|
+
closeIdleBridge(conversation) {
|
|
554
|
+
const bridging = conversation.bridge;
|
|
555
|
+
if (bridging === null || conversation.running !== null || conversation.launch !== null)
|
|
556
|
+
return;
|
|
557
|
+
conversation.bridge = null;
|
|
558
|
+
void bridging.then((bridge) => bridge.close(), () => undefined);
|
|
222
559
|
}
|
|
223
560
|
async interrupt(conversation, turnId) {
|
|
224
561
|
if (conversation.running === null || conversation.turn?.id !== turnId)
|
|
@@ -229,11 +566,13 @@ export class Conversations {
|
|
|
229
566
|
}
|
|
230
567
|
halt(conversation) {
|
|
231
568
|
if (conversation.opening !== null)
|
|
232
|
-
|
|
569
|
+
killMarked(marker(conversation.opening));
|
|
570
|
+
for (const scope of conversation.leftovers)
|
|
571
|
+
scope.kill();
|
|
233
572
|
if (conversation.running === null)
|
|
234
573
|
return;
|
|
235
574
|
conversation.running.killed = true;
|
|
236
|
-
|
|
575
|
+
conversation.running.scope.kill();
|
|
237
576
|
}
|
|
238
577
|
async kill(conversation) {
|
|
239
578
|
const running = conversation.running;
|
|
@@ -245,6 +584,7 @@ export class Conversations {
|
|
|
245
584
|
if (running === null)
|
|
246
585
|
return;
|
|
247
586
|
try {
|
|
587
|
+
running.reader.requested(option, value);
|
|
248
588
|
const answered = await running.adapter.connection.agent.request('session/set_config_option', {
|
|
249
589
|
sessionId: running.sessionId,
|
|
250
590
|
configId: option,
|
|
@@ -253,6 +593,7 @@ export class Conversations {
|
|
|
253
593
|
this.options(conversation, answered.configOptions);
|
|
254
594
|
}
|
|
255
595
|
catch (error) {
|
|
596
|
+
running.reader.refused(option);
|
|
256
597
|
logged(error);
|
|
257
598
|
}
|
|
258
599
|
}
|
|
@@ -269,58 +610,82 @@ export class Conversations {
|
|
|
269
610
|
}
|
|
270
611
|
return route;
|
|
271
612
|
}
|
|
272
|
-
async start(conversation,
|
|
613
|
+
async start(conversation, routed, sessionId, empty, bridged) {
|
|
614
|
+
const route = await routed;
|
|
615
|
+
const reader = readerFor(route.kind);
|
|
616
|
+
reader.session = sessionId;
|
|
273
617
|
const app = client({ name: '@agentshouse/kit' })
|
|
274
|
-
.onNotification('session/update', ({ params }) => this.update(conversation, params))
|
|
275
|
-
.onNotification(TURN_STARTED, (params) => params, () => void this.started(conversation))
|
|
276
|
-
.onNotification(TURN_ENDED, (params) => params, () => this.ended(conversation))
|
|
277
618
|
.onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
278
|
-
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
619
|
+
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
620
|
+
.onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
|
|
279
621
|
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
280
|
-
const
|
|
622
|
+
const bridging = this.bridged(conversation);
|
|
623
|
+
const bridge = await step(unbridged, bridging);
|
|
281
624
|
if (conversation.kills > 0) {
|
|
282
|
-
|
|
625
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
283
626
|
throw new Error(KILLED);
|
|
284
627
|
}
|
|
285
628
|
conversation.opening = bridge;
|
|
629
|
+
const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
|
|
630
|
+
const scope = new Scope(marker(bridge), randomUUID(), cli);
|
|
631
|
+
let adapter = null;
|
|
286
632
|
try {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
633
|
+
bridged(bridge);
|
|
634
|
+
await CLIS[route.kind].allowHouse(bridge.env.PATH);
|
|
635
|
+
const streamed = await step(NO_CONNECTION, work.open(cli));
|
|
636
|
+
adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
|
|
637
|
+
work.read(message);
|
|
638
|
+
this.read(conversation, reader, message);
|
|
639
|
+
}, { ...bridge.env, ...streamed }, undefined, scope));
|
|
640
|
+
void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
|
|
290
641
|
const agent = adapter.connection.agent;
|
|
291
642
|
const cwd = route.working_directory;
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
sessionId: opened.sessionId,
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const options = await this.launchSettings(running, route, opened.configOptions ?? []);
|
|
643
|
+
const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
|
|
644
|
+
const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
|
|
645
|
+
const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
|
|
646
|
+
reader.session = opened.sessionId;
|
|
647
|
+
if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
|
|
648
|
+
const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
|
|
649
|
+
await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
|
|
650
|
+
}
|
|
651
|
+
const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
652
|
+
const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
|
|
653
|
+
void work.settled().then(() => scope.record());
|
|
304
654
|
conversation.running = running;
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
this.
|
|
309
|
-
void adapter.
|
|
655
|
+
adapter.turn(conversation.open > 0);
|
|
656
|
+
conversation.session = running.sessionId;
|
|
657
|
+
conversation.options = options;
|
|
658
|
+
this.facts(conversation);
|
|
659
|
+
void adapter.lost.then((cause) => this.exited(conversation, running, cause));
|
|
310
660
|
return running.sessionId;
|
|
311
661
|
}
|
|
312
662
|
catch (error) {
|
|
313
|
-
|
|
314
|
-
|
|
663
|
+
adapter?.retire();
|
|
664
|
+
work.close();
|
|
665
|
+
conversation.leftovers.add(scope);
|
|
666
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
315
667
|
throw conversation.kills > 0 ? new Error(KILLED) : error;
|
|
316
668
|
}
|
|
317
669
|
finally {
|
|
318
670
|
conversation.opening = null;
|
|
319
671
|
}
|
|
320
672
|
}
|
|
673
|
+
async resume(conversation, kind, adapter, request, empty) {
|
|
674
|
+
try {
|
|
675
|
+
return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
|
|
679
|
+
throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
|
|
680
|
+
}
|
|
681
|
+
logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
321
685
|
async launchSettings(running, route, offered) {
|
|
322
686
|
let options = offered;
|
|
323
687
|
const settings = [
|
|
688
|
+
['mode', route.mode ?? CLIS[route.kind].fullAccess],
|
|
324
689
|
['model', route.model],
|
|
325
690
|
['thought_level', route.effort],
|
|
326
691
|
];
|
|
@@ -328,6 +693,7 @@ export class Conversations {
|
|
|
328
693
|
if (value === null)
|
|
329
694
|
continue;
|
|
330
695
|
const option = options.find((candidate) => candidate.category === category);
|
|
696
|
+
running.reader.requested(option?.id ?? category, value);
|
|
331
697
|
const answered = await running.adapter.connection.agent.request('session/set_config_option', {
|
|
332
698
|
sessionId: running.sessionId,
|
|
333
699
|
configId: option?.id ?? category,
|
|
@@ -337,23 +703,37 @@ export class Conversations {
|
|
|
337
703
|
}
|
|
338
704
|
return options;
|
|
339
705
|
}
|
|
340
|
-
exited(conversation,
|
|
341
|
-
|
|
342
|
-
if (
|
|
343
|
-
|
|
706
|
+
exited(conversation, running, cause) {
|
|
707
|
+
this.detach(conversation, running);
|
|
708
|
+
if (conversation.turn !== null) {
|
|
709
|
+
this.end(conversation, conversation.turn, exitedWith(running, cause));
|
|
710
|
+
}
|
|
711
|
+
this.next(conversation);
|
|
712
|
+
this.kit.changed();
|
|
713
|
+
}
|
|
714
|
+
detach(conversation, running) {
|
|
715
|
+
if (conversation.bridge === running.bridging)
|
|
716
|
+
conversation.bridge = null;
|
|
717
|
+
running.work.close();
|
|
718
|
+
conversation.leftovers.add(running.scope);
|
|
344
719
|
conversation.running = null;
|
|
345
720
|
conversation.commands = null;
|
|
346
|
-
conversation.
|
|
347
|
-
conversation.queued.length = 0;
|
|
721
|
+
conversation.options = null;
|
|
348
722
|
for (const [id, question] of this.questions) {
|
|
349
723
|
if (question.conversation === conversation)
|
|
350
724
|
this.questions.delete(id);
|
|
351
725
|
}
|
|
352
|
-
this.
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
|
|
726
|
+
this.process(conversation, false);
|
|
727
|
+
}
|
|
728
|
+
process(conversation, running) {
|
|
729
|
+
conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
|
|
730
|
+
}
|
|
731
|
+
facts(conversation) {
|
|
732
|
+
this.process(conversation, true);
|
|
733
|
+
if (conversation.commands !== null)
|
|
734
|
+
this.commands(conversation, conversation.commands);
|
|
735
|
+
if (conversation.options !== null)
|
|
736
|
+
this.options(conversation, conversation.options);
|
|
357
737
|
}
|
|
358
738
|
commands(conversation, commands) {
|
|
359
739
|
conversation.commands = commands;
|
|
@@ -364,11 +744,12 @@ export class Conversations {
|
|
|
364
744
|
options(conversation, options) {
|
|
365
745
|
if (conversation.running === null)
|
|
366
746
|
return;
|
|
747
|
+
conversation.options = options;
|
|
367
748
|
this.kit.send({ type: 'options', conversation_id: conversation.id, options });
|
|
368
749
|
}
|
|
369
750
|
deliver(conversation, turn, event, body) {
|
|
370
751
|
this.reporting++;
|
|
371
|
-
const delivered = conversation.reports.then(() => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, body));
|
|
752
|
+
const delivered = conversation.reports.then(async () => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, await body));
|
|
372
753
|
conversation.reports = delivered
|
|
373
754
|
.catch(() => undefined)
|
|
374
755
|
.finally(() => {
|
|
@@ -377,119 +758,97 @@ export class Conversations {
|
|
|
377
758
|
});
|
|
378
759
|
return delivered;
|
|
379
760
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
.then(() => this.admitted(conversation, turn))
|
|
385
|
-
.catch(logged);
|
|
386
|
-
return turn;
|
|
761
|
+
write(conversation, turn, operation) {
|
|
762
|
+
if (turn.ended || !this.watched)
|
|
763
|
+
return;
|
|
764
|
+
this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
|
|
387
765
|
}
|
|
388
|
-
|
|
389
|
-
turn.
|
|
390
|
-
|
|
391
|
-
this.kit.send(note);
|
|
392
|
-
if (turn.text !== '') {
|
|
393
|
-
turn.unsentFrom = 0;
|
|
394
|
-
this.draft(conversation, turn);
|
|
395
|
-
}
|
|
396
|
-
if (turn.entries !== null)
|
|
397
|
-
this.plan(conversation, turn);
|
|
766
|
+
replay(conversation, turn) {
|
|
767
|
+
for (const operation of turn.parts.operations())
|
|
768
|
+
this.write(conversation, turn, operation);
|
|
398
769
|
}
|
|
399
770
|
begin(conversation) {
|
|
400
|
-
const turn = this.
|
|
771
|
+
const turn = new Turn((operation) => this.write(conversation, turn, operation));
|
|
772
|
+
conversation.open++;
|
|
773
|
+
conversation.running?.adapter.turn(true);
|
|
774
|
+
void this.deliver(conversation, turn, 'started', {}).catch(logged);
|
|
401
775
|
conversation.turn = turn;
|
|
776
|
+
if (conversation.context !== null)
|
|
777
|
+
turn.parts.measure(conversation.context);
|
|
778
|
+
for (const part of conversation.held.splice(0))
|
|
779
|
+
turn.parts.start(part);
|
|
402
780
|
this.kit.changed();
|
|
403
781
|
return turn;
|
|
404
782
|
}
|
|
405
|
-
|
|
406
|
-
if (turn.ended)
|
|
407
|
-
return;
|
|
408
|
-
const segment = turn.segment;
|
|
409
|
-
turn.segment = null;
|
|
410
|
-
if (segment?.phase === 'note')
|
|
411
|
-
this.note(conversation, turn, segment.text);
|
|
412
|
-
else if (segment !== null)
|
|
413
|
-
this.answer(conversation, turn, segment.text);
|
|
414
|
-
this.end(conversation, turn, { text: turn.text });
|
|
415
|
-
}
|
|
416
|
-
end(conversation, turn, outcome) {
|
|
783
|
+
end(conversation, turn, failed, notice = null) {
|
|
417
784
|
if (turn.ended)
|
|
418
785
|
return;
|
|
786
|
+
if (failed !== null)
|
|
787
|
+
logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
|
|
788
|
+
if (conversation.turn === turn) {
|
|
789
|
+
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
790
|
+
this.apply(conversation, turn, event);
|
|
791
|
+
}
|
|
792
|
+
if (notice !== null)
|
|
793
|
+
turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
|
|
419
794
|
turn.ended = true;
|
|
420
795
|
conversation.open--;
|
|
796
|
+
conversation.running?.adapter.turn(conversation.open > 0);
|
|
421
797
|
if (conversation.turn === turn)
|
|
422
798
|
conversation.turn = null;
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
799
|
+
const body = Promise.allSettled(turn.uploads).then(() => ({
|
|
800
|
+
parts: turn.parts.saved(),
|
|
801
|
+
context: turn.parts.context,
|
|
802
|
+
...(failed === null ? {} : { failed: failed.failure }),
|
|
803
|
+
}));
|
|
804
|
+
void this.deliver(conversation, turn, 'ended', body).catch(logged);
|
|
805
|
+
this.next(conversation);
|
|
427
806
|
this.kit.changed();
|
|
428
807
|
}
|
|
429
|
-
|
|
430
|
-
if (conversation.open > 0)
|
|
431
|
-
conversation.late.push(cause);
|
|
432
|
-
else
|
|
433
|
-
this.end(conversation, this.report(conversation), { failed: cause });
|
|
434
|
-
}
|
|
435
|
-
prompt(conversation, prompt) {
|
|
808
|
+
prompt(conversation, prompt, taken) {
|
|
436
809
|
const running = conversation.running;
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
this.
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
else
|
|
448
|
-
this.end(conversation,
|
|
810
|
+
const turn = this.begin(conversation);
|
|
811
|
+
turn.prompted = true;
|
|
812
|
+
turn.taken = taken;
|
|
813
|
+
conversation.prompted = running.sessionId;
|
|
814
|
+
running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
|
|
815
|
+
this.took(turn);
|
|
816
|
+
const notice = answeredFailure(answer);
|
|
817
|
+
if (notice !== null) {
|
|
818
|
+
this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
|
|
822
|
+
}
|
|
823
|
+
}, async (error) => {
|
|
824
|
+
if (running.adapter.connection.signal.aborted) {
|
|
825
|
+
this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
this.took(turn);
|
|
829
|
+
if (conversation.open === 1 && conversation.running === running) {
|
|
830
|
+
running.adapter.retire();
|
|
831
|
+
this.detach(conversation, running);
|
|
832
|
+
}
|
|
833
|
+
this.end(conversation, turn, answeredWith(error));
|
|
449
834
|
}
|
|
450
835
|
});
|
|
451
836
|
}
|
|
452
|
-
attach(turn, sent) {
|
|
453
|
-
turn.prompt = sent;
|
|
454
|
-
sent.turn = turn;
|
|
455
|
-
}
|
|
456
|
-
answered(conversation, sent, failed) {
|
|
457
|
-
const turn = sent.turn;
|
|
458
|
-
if (turn === null) {
|
|
459
|
-
const queued = conversation.queued.indexOf(sent);
|
|
460
|
-
if (queued < 0)
|
|
461
|
-
return;
|
|
462
|
-
conversation.queued.splice(queued, 1);
|
|
463
|
-
if (failed !== null)
|
|
464
|
-
this.failed(conversation, failed);
|
|
465
|
-
}
|
|
466
|
-
else if (failed !== null) {
|
|
467
|
-
if (turn.ended)
|
|
468
|
-
this.failed(conversation, failed);
|
|
469
|
-
else
|
|
470
|
-
this.end(conversation, turn, { failed });
|
|
471
|
-
}
|
|
472
|
-
else if (turn.prompt === sent) {
|
|
473
|
-
this.finish(conversation, turn);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
837
|
started(conversation) {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const turn = this.begin(conversation);
|
|
480
|
-
const queued = conversation.queued.shift();
|
|
481
|
-
if (queued !== undefined)
|
|
482
|
-
this.attach(turn, queued);
|
|
838
|
+
const turn = conversation.turn ?? this.begin(conversation);
|
|
839
|
+
this.took(turn);
|
|
483
840
|
return turn;
|
|
484
841
|
}
|
|
485
842
|
ended(conversation) {
|
|
486
843
|
const turn = conversation.turn;
|
|
487
844
|
if (turn === null)
|
|
488
845
|
return;
|
|
489
|
-
if (turn.
|
|
490
|
-
this.
|
|
846
|
+
if (!turn.prompted) {
|
|
847
|
+
this.end(conversation, turn, null);
|
|
491
848
|
}
|
|
492
849
|
else {
|
|
850
|
+
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
851
|
+
this.apply(conversation, turn, event);
|
|
493
852
|
conversation.turn = null;
|
|
494
853
|
this.kit.changed();
|
|
495
854
|
}
|
|
@@ -538,101 +897,114 @@ export class Conversations {
|
|
|
538
897
|
if (content !== null)
|
|
539
898
|
this.questions.get(interactionId)?.answer({ action: 'accept', content });
|
|
540
899
|
}
|
|
541
|
-
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
900
|
+
read(conversation, reader, message) {
|
|
901
|
+
for (const event of reader.read(message))
|
|
902
|
+
this.event(conversation, event);
|
|
903
|
+
}
|
|
904
|
+
event(conversation, event) {
|
|
905
|
+
switch (event.kind) {
|
|
906
|
+
case 'started':
|
|
907
|
+
this.started(conversation);
|
|
908
|
+
return;
|
|
909
|
+
case 'ended':
|
|
910
|
+
this.ended(conversation);
|
|
911
|
+
return;
|
|
912
|
+
case 'commands':
|
|
913
|
+
this.commands(conversation, event.commands);
|
|
914
|
+
return;
|
|
915
|
+
case 'options':
|
|
916
|
+
this.options(conversation, event.options);
|
|
917
|
+
return;
|
|
918
|
+
case 'context':
|
|
919
|
+
conversation.context = { used: event.used, window: event.window };
|
|
920
|
+
conversation.turn?.parts.measure(conversation.context);
|
|
921
|
+
return;
|
|
922
|
+
case 'subagent-done':
|
|
923
|
+
this.subagentDone(conversation, event.id, event.result);
|
|
924
|
+
return;
|
|
925
|
+
case 'marker':
|
|
926
|
+
case 'image':
|
|
927
|
+
if (conversation.turn !== null)
|
|
928
|
+
break;
|
|
929
|
+
if (event.kind === 'marker' && event.marker === 'job')
|
|
930
|
+
conversation.held.push({ type: 'marker', marker: 'job', text: event.text });
|
|
931
|
+
return;
|
|
565
932
|
}
|
|
933
|
+
this.apply(conversation, this.started(conversation), event);
|
|
566
934
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
conversation.jobs.delete(job.id);
|
|
572
|
-
this.kit.changed();
|
|
935
|
+
took(turn) {
|
|
936
|
+
const taken = turn.taken;
|
|
937
|
+
turn.taken = null;
|
|
938
|
+
taken?.();
|
|
573
939
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
940
|
+
apply(conversation, turn, event) {
|
|
941
|
+
switch (event.kind) {
|
|
942
|
+
case 'text':
|
|
943
|
+
case 'reasoning':
|
|
944
|
+
turn.parts.write(event.kind, event.id, event.text);
|
|
945
|
+
return;
|
|
946
|
+
case 'command':
|
|
947
|
+
turn.parts.command();
|
|
948
|
+
return;
|
|
949
|
+
case 'subagent': {
|
|
950
|
+
const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
|
|
951
|
+
conversation.subagents.set(event.id, { description: event.description, turn, index });
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
case 'message':
|
|
955
|
+
turn.parts.start({ type: 'message', command: event.command, target: event.target });
|
|
956
|
+
return;
|
|
957
|
+
case 'marker':
|
|
958
|
+
turn.parts.start({ type: 'marker', marker: event.marker, text: event.text });
|
|
959
|
+
return;
|
|
960
|
+
case 'plan':
|
|
961
|
+
turn.parts.plan(event.entries);
|
|
962
|
+
return;
|
|
963
|
+
case 'image':
|
|
964
|
+
turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
|
|
965
|
+
this.kit.changed();
|
|
966
|
+
return;
|
|
582
967
|
}
|
|
583
|
-
turn.segment ??= { messageId, phase, text: '' };
|
|
584
|
-
turn.segment.text += text;
|
|
585
968
|
}
|
|
586
|
-
|
|
587
|
-
const
|
|
588
|
-
if (
|
|
969
|
+
subagentDone(conversation, id, result) {
|
|
970
|
+
const subagent = conversation.subagents.get(id);
|
|
971
|
+
if (subagent === undefined)
|
|
589
972
|
return;
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
note(conversation, turn, text) {
|
|
594
|
-
if (text.trim() === '')
|
|
973
|
+
conversation.subagents.delete(id);
|
|
974
|
+
if (subagent.turn === conversation.turn) {
|
|
975
|
+
subagent.turn.parts.done(subagent.index, result);
|
|
595
976
|
return;
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
977
|
+
}
|
|
978
|
+
const part = { type: 'subagent', description: subagent.description, status: 'done', result };
|
|
979
|
+
if (conversation.turn === null)
|
|
980
|
+
conversation.held.push(part);
|
|
599
981
|
else
|
|
600
|
-
turn.
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
982
|
+
conversation.turn.parts.start(part);
|
|
983
|
+
}
|
|
984
|
+
async image(conversation, turn, reserved, data, mediaType) {
|
|
985
|
+
const bytes = Buffer.from(data, 'base64');
|
|
986
|
+
const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
|
|
987
|
+
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
988
|
+
try {
|
|
989
|
+
const declared = (await this.deliver(conversation, turn, 'images', {
|
|
990
|
+
name,
|
|
991
|
+
media_type: mediaType,
|
|
992
|
+
bytes: bytes.length,
|
|
993
|
+
sha256,
|
|
994
|
+
}));
|
|
995
|
+
const saved = await uploaded(declared.upload, {
|
|
996
|
+
name,
|
|
997
|
+
bytes: bytes.length,
|
|
998
|
+
sha256,
|
|
999
|
+
read: (start, end) => [bytes.subarray(start, end)],
|
|
1000
|
+
send: (url, start, end, headers) => sentPart(url, bytes.subarray(start, end), headers),
|
|
1001
|
+
finish: (upload, partSha256) => this.kit.house.deliver('/kit/uploads/finish', { upload, part_sha256: partSha256 }),
|
|
1002
|
+
});
|
|
1003
|
+
turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
|
|
621
1004
|
}
|
|
622
|
-
|
|
623
|
-
|
|
1005
|
+
catch (error) {
|
|
1006
|
+
const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
|
|
1007
|
+
turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
|
|
624
1008
|
}
|
|
625
1009
|
}
|
|
626
|
-
plan(conversation, turn) {
|
|
627
|
-
const sent = this.kit.send({
|
|
628
|
-
type: 'plan',
|
|
629
|
-
conversation_id: conversation.id,
|
|
630
|
-
turn_id: turn.id,
|
|
631
|
-
sequence: turn.planSequence + 1,
|
|
632
|
-
from: 0,
|
|
633
|
-
steps: turn.entries.map((entry) => ({ label: entry.content, priority: entry.priority, status: entry.status })),
|
|
634
|
-
});
|
|
635
|
-
if (sent)
|
|
636
|
-
turn.planSequence++;
|
|
637
|
-
}
|
|
638
1010
|
}
|