@agentshouse/kit 0.2.1-c8179f06883b6 → 0.2.1-c83d468784cc9
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 +37 -36
- package/dist/acp.js +90 -19
- package/dist/afk.js +17 -0
- package/dist/agents.js +154 -48
- package/dist/api.js +5 -5
- package/dist/app-git.js +10 -26
- package/dist/authority-main.js +3 -3
- package/dist/bridge.js +7 -10
- package/dist/building.js +50 -0
- package/dist/calls.js +71 -0
- package/dist/chatgpt.js +32 -0
- package/dist/chunks.js +49 -0
- package/dist/clis.js +57 -42
- package/dist/connection.js +105 -0
- package/dist/conversations.js +572 -182
- package/dist/copies.js +350 -162
- package/dist/end.js +15 -4
- package/dist/events.js +70 -8
- package/dist/files.js +139 -31
- package/dist/git.js +57 -21
- package/dist/home.js +56 -12
- package/dist/house-cli.js +299 -0
- package/dist/house-main.js +7 -228
- package/dist/instructions.js +7 -11
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -67
- package/dist/login.js +43 -19
- package/dist/logout.js +1 -3
- package/dist/mcp.js +6 -2
- package/dist/parts.js +10 -0
- package/dist/placement.js +19 -36
- package/dist/relay.js +21 -0
- package/dist/resident.js +100 -44
- package/dist/scope.js +15 -4
- package/dist/scripts.js +255 -0
- package/dist/secret-input.js +1 -1
- package/dist/service.js +23 -1
- package/dist/sign-in.js +22 -39
- package/dist/silence.js +28 -0
- package/dist/skills.js +49 -35
- package/dist/transcription.js +85 -45
- package/dist/translate.js +2 -8
- package/dist/update.js +113 -126
- package/dist/wire.js +37 -3
- package/dist/work.js +3 -5
- package/package.json +20 -20
- package/dist/bootstrap.json +0 -24
- package/dist/clis-main.js +0 -23
- package/dist/codex.js +0 -61
- package/dist/stream.js +0 -77
- 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,43 +1,128 @@
|
|
|
1
|
-
import { client, } from '@agentclientprotocol/sdk';
|
|
1
|
+
import { client, RequestError, } from '@agentclientprotocol/sdk';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { mkdir } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { unlessAborted } from './abort.js';
|
|
6
|
-
import { startAdapter } from './acp.js';
|
|
6
|
+
import { signedOut, startAdapter } from './acp.js';
|
|
7
|
+
import { formAnswered, grokQuestionAnswered, permissionAllowed, PLAN_APPROVED } from './afk.js';
|
|
8
|
+
import { finalRefusal, HouseRefusal } from './api.js';
|
|
7
9
|
import { openBridge } from './bridge.js';
|
|
8
10
|
import { CLIS } from './clis.js';
|
|
9
|
-
import { readerFor } from './events.js';
|
|
10
|
-
import {
|
|
11
|
+
import { answeredFailure, readerFor } from './events.js';
|
|
12
|
+
import { inspectedFile, linkedPaths, storeRefusal, uploaded } from './files.js';
|
|
11
13
|
import { agentBase } from './home.js';
|
|
12
14
|
import { instructions } from './instructions.js';
|
|
13
|
-
import { Parts } from './parts.js';
|
|
15
|
+
import { addedTokens, Parts } from './parts.js';
|
|
16
|
+
import { refusalOf } from './refusals.js';
|
|
17
|
+
import { relayed } from './relay.js';
|
|
14
18
|
import { killMarked, Scope } from './scope.js';
|
|
15
19
|
import { holdSecretInput } from './secret-input.js';
|
|
16
|
-
import { createHowWeWork } from './skills.js';
|
|
17
20
|
import { transcribe } from './transcription.js';
|
|
18
21
|
import { workFor } from './work.js';
|
|
19
22
|
const KILLED = 'the conversation was killed';
|
|
20
|
-
|
|
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
|
+
// A reader tells a working run from a stopped one by the minute, and House keeps no finer figure, so a run reports no oftener.
|
|
39
|
+
const ACTIVITY_REPORT_MS = 60_000;
|
|
40
|
+
class NotStarted extends Error {
|
|
41
|
+
}
|
|
42
|
+
class ResumeRefused extends NotStarted {
|
|
43
|
+
}
|
|
44
|
+
async function step(reason, work) {
|
|
45
|
+
try {
|
|
46
|
+
return await work;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function unopened(reason) {
|
|
53
|
+
return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
|
|
54
|
+
}
|
|
55
|
+
// A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
|
|
56
|
+
const STARTING_SHOWN_AFTER_MS = 1_000;
|
|
57
|
+
function unbridged(error) {
|
|
58
|
+
return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
|
|
59
|
+
}
|
|
60
|
+
const EXIT_PLAN_MODE = {
|
|
61
|
+
parse(value) {
|
|
62
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
63
|
+
throw new Error('_x.ai/exit_plan_mode needs an object');
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
const GROK_QUESTION = {
|
|
69
|
+
parse(value) {
|
|
70
|
+
return value;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'done', 'option', 'answer']);
|
|
21
74
|
function causeOf(error) {
|
|
22
75
|
return error instanceof Error ? error.message : String(error);
|
|
23
76
|
}
|
|
77
|
+
function answeredWith(error) {
|
|
78
|
+
return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
|
|
79
|
+
}
|
|
80
|
+
function exitedWith(running, cause) {
|
|
81
|
+
if (running.killed)
|
|
82
|
+
return { failure: 'killed', cause: KILLED };
|
|
83
|
+
return { failure: running.adapter.silent() ? 'silent' : 'exited', cause };
|
|
84
|
+
}
|
|
24
85
|
function answerMessage(request, response) {
|
|
25
86
|
return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
|
|
26
87
|
}
|
|
88
|
+
async function sentPart(url, bytes, headers) {
|
|
89
|
+
const answer = await fetch(relayed(url), { method: 'PUT', headers, body: bytes });
|
|
90
|
+
await answer.arrayBuffer();
|
|
91
|
+
if (!answer.ok)
|
|
92
|
+
throw storeRefusal('upload', answer.status);
|
|
93
|
+
}
|
|
27
94
|
function logged(error) {
|
|
28
95
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
29
96
|
}
|
|
97
|
+
function refusal(conversation, error) {
|
|
98
|
+
if (conversation.kills > 0)
|
|
99
|
+
return KILLED;
|
|
100
|
+
const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
|
|
101
|
+
logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
|
|
102
|
+
return causeOf(error);
|
|
103
|
+
}
|
|
104
|
+
function audioOf(result) {
|
|
105
|
+
if (result.isError === true)
|
|
106
|
+
return null;
|
|
107
|
+
const file = inspectedFile(result.content.map((content) => content.text).join('\n'));
|
|
108
|
+
return file?.media_type?.startsWith('audio/') === true ? file : null;
|
|
109
|
+
}
|
|
30
110
|
function marker(bridge) {
|
|
31
111
|
return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
|
|
32
112
|
}
|
|
113
|
+
function steers(adapter) {
|
|
114
|
+
return adapter.initialized._meta?.steering?.supported === true;
|
|
115
|
+
}
|
|
33
116
|
class Turn {
|
|
34
117
|
id = randomUUID();
|
|
35
118
|
parts;
|
|
36
|
-
admitted = false;
|
|
37
119
|
questions = 0;
|
|
38
120
|
secrets = 0;
|
|
39
121
|
prompted = false;
|
|
122
|
+
steered = false;
|
|
123
|
+
taken = null;
|
|
40
124
|
ended = false;
|
|
125
|
+
quietUntil = Infinity;
|
|
41
126
|
images = 0;
|
|
42
127
|
uploads = [];
|
|
43
128
|
constructor(emit) {
|
|
@@ -50,20 +135,33 @@ class Conversation {
|
|
|
50
135
|
session = null;
|
|
51
136
|
turn = null;
|
|
52
137
|
commands = null;
|
|
138
|
+
options = null;
|
|
139
|
+
exitUntold = false;
|
|
140
|
+
afk = false;
|
|
53
141
|
context = null;
|
|
142
|
+
tokens = null;
|
|
54
143
|
leftovers = new Set();
|
|
55
144
|
waiting = [];
|
|
145
|
+
done = false;
|
|
56
146
|
sending = null;
|
|
57
147
|
subagents = new Map();
|
|
58
148
|
held = [];
|
|
59
149
|
opening = null;
|
|
150
|
+
bridge = null;
|
|
151
|
+
launch = null;
|
|
152
|
+
prompted = null;
|
|
60
153
|
kills = 0;
|
|
61
154
|
killed = new AbortController();
|
|
62
155
|
interrupts = 0;
|
|
63
156
|
interrupted = new AbortController();
|
|
64
|
-
|
|
157
|
+
turns = new Set();
|
|
65
158
|
queue = Promise.resolve();
|
|
66
159
|
reports = Promise.resolve();
|
|
160
|
+
starting = null;
|
|
161
|
+
preparing = null;
|
|
162
|
+
told = null;
|
|
163
|
+
untold = false;
|
|
164
|
+
shown = null;
|
|
67
165
|
constructor(id) {
|
|
68
166
|
this.id = id;
|
|
69
167
|
}
|
|
@@ -79,9 +177,7 @@ function asksSecret(request) {
|
|
|
79
177
|
export class Conversations {
|
|
80
178
|
kit;
|
|
81
179
|
conversations = new Map();
|
|
82
|
-
acks = new Map();
|
|
83
180
|
questions = new Map();
|
|
84
|
-
carrying = 0;
|
|
85
181
|
reporting = 0;
|
|
86
182
|
watched = false;
|
|
87
183
|
constructor(kit) {
|
|
@@ -89,43 +185,34 @@ export class Conversations {
|
|
|
89
185
|
// A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
|
|
90
186
|
setInterval(() => this.look(), 2_000);
|
|
91
187
|
}
|
|
92
|
-
|
|
93
|
-
this.
|
|
94
|
-
|
|
95
|
-
(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
this.acks.set(input.input_id, work);
|
|
112
|
-
return work;
|
|
113
|
-
})();
|
|
114
|
-
carried
|
|
115
|
-
.then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
|
|
116
|
-
.catch(logged)
|
|
117
|
-
.finally(() => {
|
|
118
|
-
this.carrying--;
|
|
119
|
-
this.kit.changed();
|
|
188
|
+
call(call) {
|
|
189
|
+
void this.kit.calls.carry(call.input_id, () => {
|
|
190
|
+
const conversation = this.conversation(call.conversation_id);
|
|
191
|
+
if (call.kind === 'open' || call.kind === 'message')
|
|
192
|
+
conversation.afk = call.afk === true;
|
|
193
|
+
if (call.kind === 'kill') {
|
|
194
|
+
conversation.kills++;
|
|
195
|
+
conversation.killed.abort(new Error(KILLED));
|
|
196
|
+
for (const waiting of conversation.waiting.splice(0))
|
|
197
|
+
waiting.settle({ refused: KILLED });
|
|
198
|
+
this.halt(conversation);
|
|
199
|
+
}
|
|
200
|
+
if (call.kind === 'interrupt') {
|
|
201
|
+
conversation.interrupts++;
|
|
202
|
+
conversation.interrupted.abort();
|
|
203
|
+
}
|
|
204
|
+
const handled = conversation.queue.then(() => this.carry(conversation, call));
|
|
205
|
+
conversation.queue = handled.catch(() => undefined);
|
|
206
|
+
return handled.then(async ({ result }) => ({ result: await result }));
|
|
120
207
|
});
|
|
121
208
|
}
|
|
122
209
|
idle() {
|
|
123
|
-
if (this.
|
|
210
|
+
if (this.reporting > 0)
|
|
124
211
|
return false;
|
|
125
212
|
for (const conversation of this.conversations.values()) {
|
|
126
213
|
const turn = conversation.turn;
|
|
127
214
|
const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
|
|
128
|
-
if (conversation.
|
|
215
|
+
if (conversation.turns.size > (asking ? 1 : 0))
|
|
129
216
|
return false;
|
|
130
217
|
}
|
|
131
218
|
const conversations = [...this.conversations.values()];
|
|
@@ -134,7 +221,7 @@ export class Conversations {
|
|
|
134
221
|
return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
|
|
135
222
|
}
|
|
136
223
|
quiet() {
|
|
137
|
-
return this.idle() && [...this.conversations.values()].every((conversation) => conversation.
|
|
224
|
+
return this.idle() && [...this.conversations.values()].every((conversation) => conversation.turns.size === 0);
|
|
138
225
|
}
|
|
139
226
|
look() {
|
|
140
227
|
for (const conversation of this.conversations.values()) {
|
|
@@ -149,6 +236,16 @@ export class Conversations {
|
|
|
149
236
|
}
|
|
150
237
|
opened() {
|
|
151
238
|
this.watched = false;
|
|
239
|
+
for (const conversation of this.conversations.values()) {
|
|
240
|
+
if (conversation.running !== null) {
|
|
241
|
+
this.facts(conversation);
|
|
242
|
+
this.subagentsChanged(conversation);
|
|
243
|
+
}
|
|
244
|
+
else if (conversation.exitUntold)
|
|
245
|
+
this.process(conversation, false);
|
|
246
|
+
if (conversation.untold || conversation.told !== null)
|
|
247
|
+
this.retell(conversation);
|
|
248
|
+
}
|
|
152
249
|
}
|
|
153
250
|
watching(watched) {
|
|
154
251
|
const resend = watched && !this.watched;
|
|
@@ -168,130 +265,319 @@ export class Conversations {
|
|
|
168
265
|
}
|
|
169
266
|
return conversation;
|
|
170
267
|
}
|
|
171
|
-
async carry(conversation,
|
|
172
|
-
if (
|
|
173
|
-
return
|
|
174
|
-
if (
|
|
175
|
-
return this.message(conversation,
|
|
176
|
-
if (
|
|
268
|
+
async carry(conversation, call) {
|
|
269
|
+
if (call.kind === 'open')
|
|
270
|
+
return this.open(conversation, call);
|
|
271
|
+
if (call.kind === 'message')
|
|
272
|
+
return this.message(conversation, call);
|
|
273
|
+
if (call.kind === 'done') {
|
|
274
|
+
conversation.done = true;
|
|
275
|
+
this.next(conversation);
|
|
276
|
+
return { result: Promise.resolve({}) };
|
|
277
|
+
}
|
|
278
|
+
await this.launched(conversation);
|
|
279
|
+
if (call.kind === 'interrupt') {
|
|
177
280
|
if (--conversation.interrupts === 0)
|
|
178
281
|
conversation.interrupted = new AbortController();
|
|
179
|
-
await this.interrupt(conversation, String(
|
|
282
|
+
await this.interrupt(conversation, String(call.turn_id));
|
|
180
283
|
}
|
|
181
|
-
if (
|
|
284
|
+
if (call.kind === 'kill') {
|
|
182
285
|
await this.kill(conversation);
|
|
183
286
|
await conversation.sending;
|
|
184
287
|
if (--conversation.kills === 0)
|
|
185
288
|
conversation.killed = new AbortController();
|
|
186
289
|
}
|
|
187
|
-
if (
|
|
188
|
-
await this.option(conversation, String(
|
|
189
|
-
if (
|
|
190
|
-
const question = this.questions.get(String(
|
|
290
|
+
if (call.kind === 'option')
|
|
291
|
+
await this.option(conversation, String(call.option), call.value);
|
|
292
|
+
if (call.kind === 'answer') {
|
|
293
|
+
const question = this.questions.get(String(call.interaction_id));
|
|
191
294
|
if (question === undefined) {
|
|
192
|
-
const text = answerMessage(
|
|
193
|
-
return this.message(conversation, { ...
|
|
295
|
+
const text = answerMessage(call.request, call.response);
|
|
296
|
+
return this.message(conversation, { ...call, text, first: false, message_id: null });
|
|
194
297
|
}
|
|
195
|
-
question.answer(
|
|
298
|
+
question.answer(call.response);
|
|
196
299
|
}
|
|
197
|
-
return {
|
|
300
|
+
return { result: Promise.resolve({}) };
|
|
198
301
|
}
|
|
199
|
-
async open(conversation,
|
|
200
|
-
|
|
201
|
-
|
|
302
|
+
async open(conversation, call) {
|
|
303
|
+
await this.stop(conversation);
|
|
304
|
+
const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
|
|
305
|
+
return {
|
|
306
|
+
result: launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) })),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
async stop(conversation) {
|
|
310
|
+
const running = conversation.running;
|
|
311
|
+
running?.scope.kill();
|
|
312
|
+
await running?.adapter.exited;
|
|
313
|
+
}
|
|
314
|
+
launch(conversation, route, sessionId, empty) {
|
|
315
|
+
let opened;
|
|
316
|
+
let failed;
|
|
317
|
+
const bridge = new Promise((resolve, reject) => {
|
|
318
|
+
opened = resolve;
|
|
319
|
+
failed = reject;
|
|
320
|
+
});
|
|
321
|
+
bridge.catch(() => undefined);
|
|
322
|
+
const session = this.start(conversation, route, sessionId, empty, opened);
|
|
323
|
+
session.catch(failed);
|
|
324
|
+
const launch = { bridge, session };
|
|
325
|
+
conversation.launch = launch;
|
|
326
|
+
void session
|
|
327
|
+
.catch(() => undefined)
|
|
328
|
+
.then(() => {
|
|
329
|
+
if (conversation.launch !== launch)
|
|
330
|
+
return;
|
|
331
|
+
conversation.launch = null;
|
|
332
|
+
this.closeIdleBridge(conversation);
|
|
333
|
+
this.next(conversation);
|
|
334
|
+
});
|
|
335
|
+
this.next(conversation);
|
|
336
|
+
return launch;
|
|
337
|
+
}
|
|
338
|
+
async launched(conversation) {
|
|
339
|
+
while (conversation.launch !== null)
|
|
340
|
+
await conversation.launch.session.catch(() => undefined);
|
|
341
|
+
}
|
|
342
|
+
instructed(conversation, route) {
|
|
343
|
+
const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
|
|
344
|
+
if (bridge === undefined)
|
|
345
|
+
return null;
|
|
346
|
+
const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
|
|
347
|
+
read.catch(() => undefined);
|
|
348
|
+
return read;
|
|
349
|
+
}
|
|
350
|
+
async message(conversation, call) {
|
|
351
|
+
const content = [
|
|
352
|
+
...(typeof call.recent === 'string' ? [{ type: 'text', text: call.recent }] : []),
|
|
353
|
+
{ type: 'text', text: String(call.text) },
|
|
354
|
+
];
|
|
355
|
+
let route;
|
|
356
|
+
let instructing = null;
|
|
202
357
|
try {
|
|
203
|
-
|
|
358
|
+
route = await this.route(conversation, call.agent_id);
|
|
359
|
+
if (call.first === true)
|
|
360
|
+
instructing = this.instructed(conversation, route);
|
|
361
|
+
if (call.message_id !== null)
|
|
362
|
+
content.push(...(await this.transcribed(conversation, String(call.message_id), String(call.text))));
|
|
363
|
+
if (conversation.kills > 0)
|
|
364
|
+
throw new Error(KILLED);
|
|
204
365
|
}
|
|
205
366
|
catch (error) {
|
|
206
|
-
|
|
367
|
+
this.closeIdleBridge(conversation);
|
|
368
|
+
this.prepares(conversation, null);
|
|
369
|
+
return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
|
|
207
370
|
}
|
|
371
|
+
conversation.preparing = null;
|
|
372
|
+
const { promise, resolve: settle } = Promise.withResolvers();
|
|
373
|
+
const waiting = { call, route, content, instructing, settle };
|
|
374
|
+
if (call.at_once === true && conversation.turns.size > 0)
|
|
375
|
+
await this.atOnce(conversation, waiting);
|
|
376
|
+
else
|
|
377
|
+
conversation.waiting.push(waiting);
|
|
378
|
+
this.next(conversation);
|
|
379
|
+
return { result: promise };
|
|
208
380
|
}
|
|
209
|
-
async
|
|
210
|
-
|
|
211
|
-
|
|
381
|
+
async atOnce(conversation, waiting) {
|
|
382
|
+
while (conversation.sending !== null)
|
|
383
|
+
await conversation.sending;
|
|
384
|
+
const running = conversation.running;
|
|
385
|
+
if (running !== null && conversation.turns.size > 0 && steers(running.adapter)) {
|
|
386
|
+
conversation.sending = this.steer(conversation, running, waiting);
|
|
387
|
+
await conversation.sending;
|
|
388
|
+
conversation.sending = null;
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
this.ahead(conversation, waiting);
|
|
392
|
+
if (conversation.turns.size > 0)
|
|
393
|
+
await this.cancel(conversation);
|
|
394
|
+
}
|
|
395
|
+
ahead(conversation, waiting) {
|
|
396
|
+
const behind = conversation.waiting.findIndex((held) => held.call.at_once !== true);
|
|
397
|
+
conversation.waiting.splice(behind === -1 ? conversation.waiting.length : behind, 0, waiting);
|
|
398
|
+
}
|
|
399
|
+
async steer(conversation, running, waiting) {
|
|
400
|
+
let outcome;
|
|
212
401
|
try {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
content.push({ type: 'text', text: lines.join('\n') });
|
|
219
|
-
if (conversation.kills > 0)
|
|
220
|
-
throw new Error(KILLED);
|
|
402
|
+
({ outcome } = await running.adapter.connection.agent.request('_session/steering', {
|
|
403
|
+
sessionId: running.sessionId,
|
|
404
|
+
prompt: waiting.content,
|
|
405
|
+
_meta: { steering: { idleBehavior: 'promptRequired' } },
|
|
406
|
+
}));
|
|
221
407
|
}
|
|
222
408
|
catch (error) {
|
|
223
|
-
|
|
409
|
+
outcome = error;
|
|
410
|
+
}
|
|
411
|
+
if (outcome === 'promptRequired') {
|
|
412
|
+
this.ahead(conversation, waiting);
|
|
413
|
+
}
|
|
414
|
+
else if (outcome === 'injected' || outcome === 'startedNewTurn') {
|
|
415
|
+
if (outcome === 'injected') {
|
|
416
|
+
for (const turn of conversation.turns)
|
|
417
|
+
turn.steered ||= turn.prompted;
|
|
418
|
+
}
|
|
419
|
+
this.kit.calls.keepResult(String(waiting.call.input_id), {});
|
|
420
|
+
waiting.settle({});
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
const cause = outcome instanceof Error ? outcome : new Error(`the CLI answered ${JSON.stringify(outcome)}`);
|
|
424
|
+
waiting.settle({ refused: refusal(conversation, new NotStarted(UNSTEERED, { cause })) });
|
|
224
425
|
}
|
|
225
|
-
return {
|
|
226
|
-
ack: new Promise((settle) => {
|
|
227
|
-
conversation.waiting.push({ input, route, content, settle });
|
|
228
|
-
this.next(conversation);
|
|
229
|
-
}),
|
|
230
|
-
};
|
|
231
426
|
}
|
|
232
427
|
next(conversation) {
|
|
233
|
-
if (conversation.sending !== null || conversation.
|
|
428
|
+
if (conversation.sending !== null || conversation.turns.size > 0)
|
|
234
429
|
return;
|
|
235
430
|
const waiting = conversation.waiting.shift();
|
|
236
|
-
if (waiting === undefined)
|
|
431
|
+
if (waiting === undefined) {
|
|
432
|
+
const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
|
|
433
|
+
if (conversation.starting !== step)
|
|
434
|
+
this.starts(conversation, step);
|
|
435
|
+
if (conversation.done && conversation.launch === null)
|
|
436
|
+
void this.stop(conversation).catch(logged);
|
|
237
437
|
return;
|
|
438
|
+
}
|
|
238
439
|
conversation.sending = this.send(conversation, waiting).finally(() => {
|
|
239
440
|
conversation.sending = null;
|
|
240
441
|
this.next(conversation);
|
|
241
442
|
});
|
|
242
443
|
}
|
|
243
444
|
async send(conversation, waiting) {
|
|
244
|
-
const {
|
|
245
|
-
let
|
|
445
|
+
const { call } = waiting;
|
|
446
|
+
let result = {};
|
|
447
|
+
let started = false;
|
|
448
|
+
let instructing = waiting.instructing;
|
|
246
449
|
const prompt = [];
|
|
247
450
|
try {
|
|
248
451
|
if (conversation.running === null) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (resumed === null)
|
|
252
|
-
ack = { provider_session_id: opened };
|
|
452
|
+
this.starts(conversation, 'session');
|
|
453
|
+
await this.launched(conversation);
|
|
253
454
|
}
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
455
|
+
if (conversation.running === null) {
|
|
456
|
+
const stored = conversation.session ?? call.provider_session_id;
|
|
457
|
+
const empty = call.first === true && conversation.prompted !== stored;
|
|
458
|
+
const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
|
|
459
|
+
if (call.first === true)
|
|
460
|
+
instructing = this.instructed(conversation, waiting.route);
|
|
461
|
+
started = (await launch.session) !== stored;
|
|
462
|
+
}
|
|
463
|
+
const session = conversation.running.sessionId;
|
|
464
|
+
if (started || (call.first === true && session !== call.provider_session_id)) {
|
|
465
|
+
result = { provider_session_id: session };
|
|
466
|
+
}
|
|
467
|
+
if (call.first === true) {
|
|
468
|
+
prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
|
|
259
469
|
}
|
|
260
470
|
if (conversation.kills > 0)
|
|
261
471
|
throw new Error(KILLED);
|
|
262
472
|
}
|
|
263
473
|
catch (error) {
|
|
264
|
-
|
|
474
|
+
this.starts(conversation, null);
|
|
475
|
+
if (started) {
|
|
265
476
|
conversation.session = null;
|
|
266
477
|
await this.kill(conversation);
|
|
267
478
|
}
|
|
268
|
-
|
|
479
|
+
const refused = refusal(conversation, error);
|
|
480
|
+
const resumeRefused = call.kind === 'message' && conversation.kills === 0 && error instanceof ResumeRefused;
|
|
481
|
+
waiting.settle(resumeRefused ? { refused, resume_refused: true } : { refused });
|
|
269
482
|
return;
|
|
270
483
|
}
|
|
271
|
-
this.
|
|
272
|
-
waiting.
|
|
484
|
+
this.starts(conversation, null);
|
|
485
|
+
this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
|
|
486
|
+
waiting.settle(result);
|
|
273
487
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
488
|
+
starts(conversation, step) {
|
|
489
|
+
conversation.starting = step;
|
|
490
|
+
if (conversation.shown !== null)
|
|
491
|
+
clearTimeout(conversation.shown);
|
|
492
|
+
conversation.shown = null;
|
|
493
|
+
if (step === null || conversation.told !== null) {
|
|
494
|
+
this.tell(conversation, step);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
conversation.shown = setTimeout(() => {
|
|
498
|
+
conversation.shown = null;
|
|
499
|
+
if (conversation.starting === step)
|
|
500
|
+
this.tell(conversation, step);
|
|
501
|
+
}, STARTING_SHOWN_AFTER_MS);
|
|
502
|
+
}
|
|
503
|
+
prepares(conversation, step) {
|
|
504
|
+
conversation.preparing = step;
|
|
505
|
+
this.next(conversation);
|
|
506
|
+
}
|
|
507
|
+
tell(conversation, step) {
|
|
508
|
+
if (conversation.told === step && !conversation.untold)
|
|
509
|
+
return;
|
|
510
|
+
conversation.told = step;
|
|
511
|
+
this.retell(conversation);
|
|
512
|
+
}
|
|
513
|
+
retell(conversation) {
|
|
514
|
+
conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
|
|
515
|
+
}
|
|
516
|
+
async transcribed(conversation, messageId, text) {
|
|
517
|
+
const paths = linkedPaths(text);
|
|
518
|
+
if (paths.length === 0)
|
|
519
|
+
return [];
|
|
520
|
+
const killed = conversation.killed.signal;
|
|
521
|
+
const interrupted = conversation.interrupted.signal;
|
|
522
|
+
const transcribing = AbortSignal.any([killed, interrupted]);
|
|
523
|
+
let signal = killed;
|
|
524
|
+
const blocks = [];
|
|
525
|
+
try {
|
|
526
|
+
const bridge = await step(unbridged, unlessAborted(this.bridged(conversation), killed));
|
|
527
|
+
const read = async (path) => audioOf(await step(unbridged, unlessAborted(bridge.tool('inspect', { path }), signal)));
|
|
528
|
+
for (const path of paths) {
|
|
529
|
+
let answered = await read(path);
|
|
530
|
+
if (answered === null)
|
|
531
|
+
continue;
|
|
532
|
+
signal = transcribing;
|
|
533
|
+
this.prepares(conversation, 'transcription');
|
|
534
|
+
const said = await transcribe(async () => {
|
|
535
|
+
const address = answered ?? (await read(path));
|
|
536
|
+
answered = null;
|
|
537
|
+
if (address === null)
|
|
538
|
+
throw new Error(`${path} is no longer an audio file`);
|
|
539
|
+
return address;
|
|
540
|
+
}, signal);
|
|
541
|
+
await step(unbridged, unlessAborted(this.kit.house.deliver('/kit/transcripts/keep', { conversation: conversation.id, message: messageId, path, text: said }, () => !signal.aborted), signal));
|
|
542
|
+
blocks.push({ type: 'text', text: `${path}\nTranscript: ${said}` });
|
|
289
543
|
}
|
|
290
544
|
}
|
|
291
|
-
|
|
545
|
+
catch (error) {
|
|
546
|
+
if (signal === transcribing && interrupted.aborted && conversation.kills === 0)
|
|
547
|
+
throw new NotStarted(INTERRUPTED, { cause: error });
|
|
548
|
+
throw error;
|
|
549
|
+
}
|
|
550
|
+
return blocks;
|
|
551
|
+
}
|
|
552
|
+
bridged(conversation) {
|
|
553
|
+
if (conversation.bridge === null) {
|
|
554
|
+
const bridging = openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
|
|
555
|
+
conversation.bridge = bridging;
|
|
556
|
+
bridging.catch(() => {
|
|
557
|
+
if (conversation.bridge === bridging)
|
|
558
|
+
conversation.bridge = null;
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
return conversation.bridge;
|
|
562
|
+
}
|
|
563
|
+
closeBridge(conversation, bridging, bridge) {
|
|
564
|
+
bridge.close();
|
|
565
|
+
if (conversation.bridge === bridging)
|
|
566
|
+
conversation.bridge = null;
|
|
567
|
+
}
|
|
568
|
+
closeIdleBridge(conversation) {
|
|
569
|
+
const bridging = conversation.bridge;
|
|
570
|
+
if (bridging === null || conversation.running !== null || conversation.launch !== null)
|
|
571
|
+
return;
|
|
572
|
+
conversation.bridge = null;
|
|
573
|
+
void bridging.then((bridge) => bridge.close(), () => undefined);
|
|
292
574
|
}
|
|
293
575
|
async interrupt(conversation, turnId) {
|
|
294
|
-
if (conversation.
|
|
576
|
+
if ([...conversation.turns].some((turn) => turn.id === turnId))
|
|
577
|
+
await this.cancel(conversation);
|
|
578
|
+
}
|
|
579
|
+
async cancel(conversation) {
|
|
580
|
+
if (conversation.running === null)
|
|
295
581
|
return;
|
|
296
582
|
await conversation.running.adapter.connection.agent.notify('session/cancel', {
|
|
297
583
|
sessionId: conversation.running.sessionId,
|
|
@@ -343,63 +629,83 @@ export class Conversations {
|
|
|
343
629
|
}
|
|
344
630
|
return route;
|
|
345
631
|
}
|
|
346
|
-
async start(conversation,
|
|
632
|
+
async start(conversation, routed, sessionId, empty, bridged) {
|
|
633
|
+
const route = await routed;
|
|
347
634
|
const reader = readerFor(route.kind);
|
|
348
635
|
reader.session = sessionId;
|
|
349
636
|
const app = client({ name: '@agentshouse/kit' })
|
|
350
|
-
.onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
351
|
-
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
637
|
+
.onRequest('session/request_permission', ({ params, signal }) => conversation.afk ? permissionAllowed(params) : this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
638
|
+
.onRequest('elicitation/create', ({ params, signal }) => conversation.afk ? formAnswered(route.kind, params) : this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
639
|
+
.onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => conversation.afk ? PLAN_APPROVED : this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal))
|
|
640
|
+
.onRequest('_x.ai/ask_user_question', GROK_QUESTION, ({ params }) => {
|
|
641
|
+
if (!conversation.afk)
|
|
642
|
+
throw RequestError.methodNotFound('_x.ai/ask_user_question');
|
|
643
|
+
return grokQuestionAnswered(params);
|
|
644
|
+
});
|
|
352
645
|
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
353
|
-
const
|
|
646
|
+
const bridging = this.bridged(conversation);
|
|
647
|
+
const bridge = await step(unbridged, bridging);
|
|
354
648
|
if (conversation.kills > 0) {
|
|
355
|
-
|
|
649
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
356
650
|
throw new Error(KILLED);
|
|
357
651
|
}
|
|
358
652
|
conversation.opening = bridge;
|
|
359
653
|
const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
|
|
360
654
|
const scope = new Scope(marker(bridge), randomUUID(), cli);
|
|
655
|
+
let adapter = null;
|
|
361
656
|
try {
|
|
362
|
-
|
|
657
|
+
bridged(bridge);
|
|
363
658
|
await CLIS[route.kind].allowHouse(bridge.env.PATH);
|
|
364
|
-
const
|
|
659
|
+
const streamed = await step(NO_CONNECTION, work.open(cli));
|
|
660
|
+
adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
|
|
365
661
|
work.read(message);
|
|
366
662
|
this.read(conversation, reader, message);
|
|
367
|
-
}, { ...bridge.env, ...
|
|
368
|
-
void adapter.exited.then(() =>
|
|
663
|
+
}, { ...bridge.env, ...streamed }, undefined, scope));
|
|
664
|
+
void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
|
|
369
665
|
const agent = adapter.connection.agent;
|
|
370
666
|
const cwd = route.working_directory;
|
|
371
667
|
const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
: { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
|
|
668
|
+
const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
|
|
669
|
+
const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
|
|
375
670
|
reader.session = opened.sessionId;
|
|
376
|
-
if (sessionId
|
|
671
|
+
if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
|
|
377
672
|
const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
|
|
378
|
-
await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
|
|
673
|
+
await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
|
|
379
674
|
}
|
|
380
|
-
const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
381
|
-
const options = await this.launchSettings(running, route, opened.configOptions ?? []);
|
|
675
|
+
const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
676
|
+
const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
|
|
382
677
|
void work.settled().then(() => scope.record());
|
|
383
678
|
conversation.running = running;
|
|
679
|
+
adapter.turn(conversation.turns.size > 0);
|
|
384
680
|
conversation.session = running.sessionId;
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
this.options(conversation, options);
|
|
389
|
-
void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
|
|
681
|
+
conversation.options = options;
|
|
682
|
+
this.facts(conversation);
|
|
683
|
+
void adapter.lost.then((cause) => this.exited(conversation, running, cause));
|
|
390
684
|
return running.sessionId;
|
|
391
685
|
}
|
|
392
686
|
catch (error) {
|
|
393
|
-
|
|
687
|
+
adapter?.retire();
|
|
394
688
|
work.close();
|
|
395
689
|
conversation.leftovers.add(scope);
|
|
396
|
-
|
|
690
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
397
691
|
throw conversation.kills > 0 ? new Error(KILLED) : error;
|
|
398
692
|
}
|
|
399
693
|
finally {
|
|
400
694
|
conversation.opening = null;
|
|
401
695
|
}
|
|
402
696
|
}
|
|
697
|
+
async resume(conversation, kind, adapter, request, empty) {
|
|
698
|
+
try {
|
|
699
|
+
return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
|
|
703
|
+
throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
|
|
704
|
+
}
|
|
705
|
+
logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
403
709
|
async launchSettings(running, route, offered) {
|
|
404
710
|
let options = offered;
|
|
405
711
|
const settings = [
|
|
@@ -421,27 +727,45 @@ export class Conversations {
|
|
|
421
727
|
}
|
|
422
728
|
return options;
|
|
423
729
|
}
|
|
424
|
-
exited(conversation,
|
|
425
|
-
const running = conversation.running;
|
|
426
|
-
if (running?.adapter !== adapter)
|
|
427
|
-
return;
|
|
730
|
+
exited(conversation, running, cause) {
|
|
428
731
|
this.detach(conversation, running);
|
|
429
732
|
if (conversation.turn !== null) {
|
|
430
|
-
this.end(conversation, conversation.turn,
|
|
733
|
+
this.end(conversation, conversation.turn, exitedWith(running, cause));
|
|
431
734
|
}
|
|
432
735
|
this.next(conversation);
|
|
433
736
|
this.kit.changed();
|
|
434
737
|
}
|
|
435
738
|
detach(conversation, running) {
|
|
739
|
+
if (conversation.bridge === running.bridging)
|
|
740
|
+
conversation.bridge = null;
|
|
436
741
|
running.work.close();
|
|
437
742
|
conversation.leftovers.add(running.scope);
|
|
438
743
|
conversation.running = null;
|
|
439
744
|
conversation.commands = null;
|
|
745
|
+
conversation.options = null;
|
|
746
|
+
if (conversation.subagents.size > 0) {
|
|
747
|
+
conversation.subagents.clear();
|
|
748
|
+
this.subagentsChanged(conversation);
|
|
749
|
+
}
|
|
440
750
|
for (const [id, question] of this.questions) {
|
|
441
751
|
if (question.conversation === conversation)
|
|
442
752
|
this.questions.delete(id);
|
|
443
753
|
}
|
|
444
|
-
this.
|
|
754
|
+
this.process(conversation, false);
|
|
755
|
+
}
|
|
756
|
+
process(conversation, running) {
|
|
757
|
+
conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
|
|
758
|
+
}
|
|
759
|
+
facts(conversation) {
|
|
760
|
+
this.process(conversation, true);
|
|
761
|
+
if (conversation.commands !== null)
|
|
762
|
+
this.commands(conversation, conversation.commands);
|
|
763
|
+
if (conversation.options !== null)
|
|
764
|
+
this.options(conversation, conversation.options);
|
|
765
|
+
}
|
|
766
|
+
subagentsChanged(conversation) {
|
|
767
|
+
const subagents = [...conversation.subagents.values()].map(({ description, activity }) => ({ description, activity }));
|
|
768
|
+
this.kit.send({ type: 'subagents', conversation_id: conversation.id, subagents });
|
|
445
769
|
}
|
|
446
770
|
commands(conversation, commands) {
|
|
447
771
|
conversation.commands = commands;
|
|
@@ -452,11 +776,12 @@ export class Conversations {
|
|
|
452
776
|
options(conversation, options) {
|
|
453
777
|
if (conversation.running === null)
|
|
454
778
|
return;
|
|
779
|
+
conversation.options = options;
|
|
455
780
|
this.kit.send({ type: 'options', conversation_id: conversation.id, options });
|
|
456
781
|
}
|
|
457
|
-
deliver(conversation, turn,
|
|
782
|
+
deliver(conversation, turn, act, body, current) {
|
|
458
783
|
this.reporting++;
|
|
459
|
-
const delivered = conversation.reports.then(async () => this.kit.house.deliver(`/kit
|
|
784
|
+
const delivered = conversation.reports.then(async () => this.kit.house.deliver(`/kit/${act}`, { conversation: conversation.id, turn: turn.id, ...(await body) }, current));
|
|
460
785
|
conversation.reports = delivered
|
|
461
786
|
.catch(() => undefined)
|
|
462
787
|
.finally(() => {
|
|
@@ -466,7 +791,7 @@ export class Conversations {
|
|
|
466
791
|
return delivered;
|
|
467
792
|
}
|
|
468
793
|
write(conversation, turn, operation) {
|
|
469
|
-
if (
|
|
794
|
+
if (turn.ended || !this.watched)
|
|
470
795
|
return;
|
|
471
796
|
this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
|
|
472
797
|
}
|
|
@@ -476,11 +801,12 @@ export class Conversations {
|
|
|
476
801
|
}
|
|
477
802
|
begin(conversation) {
|
|
478
803
|
const turn = new Turn((operation) => this.write(conversation, turn, operation));
|
|
479
|
-
conversation.
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
804
|
+
conversation.turns.add(turn);
|
|
805
|
+
conversation.running?.adapter.turn(true);
|
|
806
|
+
void this.deliver(conversation, turn, 'turns/start', {})
|
|
807
|
+
.then((answer) => {
|
|
808
|
+
if (answer.run === true)
|
|
809
|
+
turn.quietUntil = Date.now() + ACTIVITY_REPORT_MS;
|
|
484
810
|
})
|
|
485
811
|
.catch(logged);
|
|
486
812
|
conversation.turn = turn;
|
|
@@ -491,59 +817,84 @@ export class Conversations {
|
|
|
491
817
|
this.kit.changed();
|
|
492
818
|
return turn;
|
|
493
819
|
}
|
|
494
|
-
end(conversation, turn, failed) {
|
|
820
|
+
end(conversation, turn, failed, notice = null) {
|
|
495
821
|
if (turn.ended)
|
|
496
822
|
return;
|
|
823
|
+
if (failed !== null)
|
|
824
|
+
logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
|
|
497
825
|
if (conversation.turn === turn) {
|
|
498
826
|
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
499
827
|
this.apply(conversation, turn, event);
|
|
500
828
|
}
|
|
829
|
+
if (notice !== null)
|
|
830
|
+
turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
|
|
501
831
|
turn.ended = true;
|
|
502
|
-
conversation.
|
|
832
|
+
conversation.turns.delete(turn);
|
|
833
|
+
conversation.running?.adapter.turn(conversation.turns.size > 0);
|
|
503
834
|
if (conversation.turn === turn)
|
|
504
835
|
conversation.turn = null;
|
|
836
|
+
const tokens = conversation.tokens;
|
|
837
|
+
conversation.tokens = null;
|
|
505
838
|
const body = Promise.allSettled(turn.uploads).then(() => ({
|
|
506
839
|
parts: turn.parts.saved(),
|
|
507
840
|
context: turn.parts.context,
|
|
508
|
-
...
|
|
841
|
+
...(tokens === null ? {} : { tokens }),
|
|
842
|
+
...(failed === null ? {} : { failed: failed.failure }),
|
|
509
843
|
}));
|
|
510
|
-
void this.deliver(conversation, turn, '
|
|
844
|
+
void this.deliver(conversation, turn, 'turns/end', body).catch(logged);
|
|
511
845
|
this.next(conversation);
|
|
512
846
|
this.kit.changed();
|
|
513
847
|
}
|
|
514
|
-
prompt(conversation, prompt) {
|
|
848
|
+
prompt(conversation, prompt, taken) {
|
|
515
849
|
const running = conversation.running;
|
|
516
850
|
const turn = this.begin(conversation);
|
|
517
851
|
turn.prompted = true;
|
|
518
|
-
|
|
852
|
+
turn.taken = taken;
|
|
853
|
+
conversation.prompted = running.sessionId;
|
|
854
|
+
running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
|
|
855
|
+
this.took(turn);
|
|
856
|
+
const notice = answeredFailure(answer);
|
|
857
|
+
if (notice !== null) {
|
|
858
|
+
this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
|
|
862
|
+
}
|
|
863
|
+
}, async (error) => {
|
|
519
864
|
if (running.adapter.connection.signal.aborted) {
|
|
520
|
-
this.end(conversation, turn,
|
|
865
|
+
this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
|
|
521
866
|
}
|
|
522
867
|
else {
|
|
523
|
-
|
|
524
|
-
|
|
868
|
+
this.took(turn);
|
|
869
|
+
if (conversation.turns.size === 1 && conversation.turns.has(turn) && conversation.running === running) {
|
|
870
|
+
running.adapter.retire();
|
|
525
871
|
this.detach(conversation, running);
|
|
526
872
|
}
|
|
527
|
-
this.end(conversation, turn,
|
|
873
|
+
this.end(conversation, turn, answeredWith(error));
|
|
528
874
|
}
|
|
529
875
|
});
|
|
530
876
|
}
|
|
531
877
|
started(conversation) {
|
|
532
|
-
|
|
878
|
+
const turn = conversation.turn ?? this.begin(conversation);
|
|
879
|
+
this.took(turn);
|
|
880
|
+
return turn;
|
|
533
881
|
}
|
|
534
|
-
ended(conversation) {
|
|
882
|
+
ended(conversation, stop) {
|
|
535
883
|
const turn = conversation.turn;
|
|
536
884
|
if (turn === null)
|
|
537
885
|
return;
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
this.apply(conversation, turn, event);
|
|
544
|
-
conversation.turn = null;
|
|
545
|
-
this.kit.changed();
|
|
886
|
+
const held = stop !== null && !turn.steered && [...conversation.subagents.values()].some((subagent) => subagent.turn === turn);
|
|
887
|
+
if (!turn.prompted || held) {
|
|
888
|
+
this.took(turn);
|
|
889
|
+
this.end(conversation, turn, stop === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
|
|
890
|
+
return;
|
|
546
891
|
}
|
|
892
|
+
if (stop !== null)
|
|
893
|
+
return;
|
|
894
|
+
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
895
|
+
this.apply(conversation, turn, event);
|
|
896
|
+
conversation.turn = null;
|
|
897
|
+
this.kit.changed();
|
|
547
898
|
}
|
|
548
899
|
async ask(conversation, method, params, secret, signal) {
|
|
549
900
|
const turn = this.started(conversation);
|
|
@@ -551,7 +902,7 @@ export class Conversations {
|
|
|
551
902
|
const answered = new Promise((answer, fail) => {
|
|
552
903
|
this.questions.set(interactionId, { conversation, answer: (response) => answer(response) });
|
|
553
904
|
signal.addEventListener('abort', () => answer(null), { once: true });
|
|
554
|
-
this.deliver(conversation, turn, '
|
|
905
|
+
this.deliver(conversation, turn, 'conversations/interaction/ask', {
|
|
555
906
|
interaction_id: interactionId,
|
|
556
907
|
request: { method, params },
|
|
557
908
|
secret,
|
|
@@ -592,6 +943,18 @@ export class Conversations {
|
|
|
592
943
|
read(conversation, reader, message) {
|
|
593
944
|
for (const event of reader.read(message))
|
|
594
945
|
this.event(conversation, event);
|
|
946
|
+
this.reportActivity(conversation);
|
|
947
|
+
}
|
|
948
|
+
reportActivity(conversation) {
|
|
949
|
+
const turn = conversation.turn;
|
|
950
|
+
if (turn === null || Date.now() < turn.quietUntil)
|
|
951
|
+
return;
|
|
952
|
+
turn.quietUntil = Infinity;
|
|
953
|
+
void this.deliver(conversation, turn, 'turns/activity/report', {}, () => false)
|
|
954
|
+
.catch(logged)
|
|
955
|
+
.finally(() => {
|
|
956
|
+
turn.quietUntil = Date.now() + ACTIVITY_REPORT_MS;
|
|
957
|
+
});
|
|
595
958
|
}
|
|
596
959
|
event(conversation, event) {
|
|
597
960
|
switch (event.kind) {
|
|
@@ -599,7 +962,7 @@ export class Conversations {
|
|
|
599
962
|
this.started(conversation);
|
|
600
963
|
return;
|
|
601
964
|
case 'ended':
|
|
602
|
-
this.ended(conversation);
|
|
965
|
+
this.ended(conversation, event.stop);
|
|
603
966
|
return;
|
|
604
967
|
case 'commands':
|
|
605
968
|
this.commands(conversation, event.commands);
|
|
@@ -611,9 +974,15 @@ export class Conversations {
|
|
|
611
974
|
conversation.context = { used: event.used, window: event.window };
|
|
612
975
|
conversation.turn?.parts.measure(conversation.context);
|
|
613
976
|
return;
|
|
977
|
+
case 'tokens':
|
|
978
|
+
conversation.tokens = addedTokens(conversation.tokens, event.tokens);
|
|
979
|
+
return;
|
|
614
980
|
case 'subagent-done':
|
|
615
981
|
this.subagentDone(conversation, event.id, event.result);
|
|
616
982
|
return;
|
|
983
|
+
case 'subagent-activity':
|
|
984
|
+
this.subagentActivity(conversation, event.id, event.activity);
|
|
985
|
+
return;
|
|
617
986
|
case 'marker':
|
|
618
987
|
case 'image':
|
|
619
988
|
if (conversation.turn !== null)
|
|
@@ -624,6 +993,11 @@ export class Conversations {
|
|
|
624
993
|
}
|
|
625
994
|
this.apply(conversation, this.started(conversation), event);
|
|
626
995
|
}
|
|
996
|
+
took(turn) {
|
|
997
|
+
const taken = turn.taken;
|
|
998
|
+
turn.taken = null;
|
|
999
|
+
taken?.();
|
|
1000
|
+
}
|
|
627
1001
|
apply(conversation, turn, event) {
|
|
628
1002
|
switch (event.kind) {
|
|
629
1003
|
case 'text':
|
|
@@ -635,7 +1009,8 @@ export class Conversations {
|
|
|
635
1009
|
return;
|
|
636
1010
|
case 'subagent': {
|
|
637
1011
|
const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
|
|
638
|
-
conversation.subagents.set(event.id, { description: event.description, turn, index });
|
|
1012
|
+
conversation.subagents.set(event.id, { description: event.description, turn, index, activity: null });
|
|
1013
|
+
this.subagentsChanged(conversation);
|
|
639
1014
|
return;
|
|
640
1015
|
}
|
|
641
1016
|
case 'message':
|
|
@@ -658,6 +1033,7 @@ export class Conversations {
|
|
|
658
1033
|
if (subagent === undefined)
|
|
659
1034
|
return;
|
|
660
1035
|
conversation.subagents.delete(id);
|
|
1036
|
+
this.subagentsChanged(conversation);
|
|
661
1037
|
if (subagent.turn === conversation.turn) {
|
|
662
1038
|
subagent.turn.parts.done(subagent.index, result);
|
|
663
1039
|
return;
|
|
@@ -668,23 +1044,37 @@ export class Conversations {
|
|
|
668
1044
|
else
|
|
669
1045
|
conversation.turn.parts.start(part);
|
|
670
1046
|
}
|
|
1047
|
+
subagentActivity(conversation, id, activity) {
|
|
1048
|
+
const subagent = conversation.subagents.get(id);
|
|
1049
|
+
if (subagent === undefined || subagent.activity === activity)
|
|
1050
|
+
return;
|
|
1051
|
+
subagent.activity = activity;
|
|
1052
|
+
this.subagentsChanged(conversation);
|
|
1053
|
+
}
|
|
671
1054
|
async image(conversation, turn, reserved, data, mediaType) {
|
|
672
1055
|
const bytes = Buffer.from(data, 'base64');
|
|
673
1056
|
const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
|
|
1057
|
+
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
674
1058
|
try {
|
|
675
|
-
const declared = (await this.deliver(conversation, turn, 'images', {
|
|
1059
|
+
const declared = (await this.deliver(conversation, turn, 'turns/images/declare', {
|
|
676
1060
|
name,
|
|
677
1061
|
media_type: mediaType,
|
|
678
1062
|
bytes: bytes.length,
|
|
679
|
-
sha256
|
|
1063
|
+
sha256,
|
|
680
1064
|
}));
|
|
681
|
-
const saved =
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
1065
|
+
const saved = await uploaded(declared.upload, {
|
|
1066
|
+
name,
|
|
1067
|
+
bytes: bytes.length,
|
|
1068
|
+
sha256,
|
|
1069
|
+
read: (start, end) => [bytes.subarray(start, end)],
|
|
1070
|
+
send: (url, start, end, headers) => sentPart(url, bytes.subarray(start, end), headers),
|
|
1071
|
+
finish: (upload, partSha256) => this.kit.house.deliver('/kit/uploads/finish', { upload, part_sha256: partSha256 }),
|
|
1072
|
+
});
|
|
1073
|
+
turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
|
|
685
1074
|
}
|
|
686
1075
|
catch (error) {
|
|
687
|
-
|
|
1076
|
+
const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
|
|
1077
|
+
turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
|
|
688
1078
|
}
|
|
689
1079
|
}
|
|
690
1080
|
}
|