@illuminis/comprism 0.1.0
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/LICENSE +15 -0
- package/README.md +281 -0
- package/out/agent/command.d.ts +86 -0
- package/out/agent/command.js +259 -0
- package/out/agent/render.d.ts +97 -0
- package/out/agent/render.js +255 -0
- package/out/agent/session.d.ts +175 -0
- package/out/agent/session.js +573 -0
- package/out/commands/ask.d.ts +1 -0
- package/out/commands/ask.js +146 -0
- package/out/commands/codemap.d.ts +2 -0
- package/out/commands/codemap.js +151 -0
- package/out/commands/commands-thin.d.ts +39 -0
- package/out/commands/commands-thin.js +182 -0
- package/out/commands/install.d.ts +163 -0
- package/out/commands/install.js +543 -0
- package/out/commands/keys.d.ts +55 -0
- package/out/commands/keys.js +344 -0
- package/out/commands/login.d.ts +9 -0
- package/out/commands/login.js +384 -0
- package/out/commands/repl.d.ts +1 -0
- package/out/commands/repl.js +752 -0
- package/out/commands/settings.d.ts +21 -0
- package/out/commands/settings.js +244 -0
- package/out/commands/welcome.d.ts +1 -0
- package/out/commands/welcome.js +196 -0
- package/out/executor/documents.d.ts +40 -0
- package/out/executor/documents.js +170 -0
- package/out/executor/files.d.ts +2 -0
- package/out/executor/files.js +360 -0
- package/out/executor/git.d.ts +48 -0
- package/out/executor/git.js +132 -0
- package/out/executor/hooks.d.ts +67 -0
- package/out/executor/hooks.js +247 -0
- package/out/executor/index.d.ts +29 -0
- package/out/executor/index.js +221 -0
- package/out/executor/notebook.d.ts +2 -0
- package/out/executor/notebook.js +147 -0
- package/out/executor/paths.d.ts +15 -0
- package/out/executor/paths.js +126 -0
- package/out/executor/shell.d.ts +41 -0
- package/out/executor/shell.js +336 -0
- package/out/graph/build.d.ts +45 -0
- package/out/graph/build.js +91 -0
- package/out/graph/facts.d.ts +47 -0
- package/out/graph/facts.js +12 -0
- package/out/graph/files.d.ts +45 -0
- package/out/graph/files.js +207 -0
- package/out/graph/read-locales.d.ts +29 -0
- package/out/graph/read-locales.js +246 -0
- package/out/graph/read-python.d.ts +11 -0
- package/out/graph/read-python.js +115 -0
- package/out/graph/read-typescript.d.ts +16 -0
- package/out/graph/read-typescript.js +292 -0
- package/out/graph/sync.d.ts +66 -0
- package/out/graph/sync.js +242 -0
- package/out/lib/attach.d.ts +62 -0
- package/out/lib/attach.js +228 -0
- package/out/lib/config.d.ts +93 -0
- package/out/lib/config.js +198 -0
- package/out/lib/connection.d.ts +73 -0
- package/out/lib/connection.js +188 -0
- package/out/lib/gateway.d.ts +239 -0
- package/out/lib/gateway.js +171 -0
- package/out/lib/prompt.d.ts +34 -0
- package/out/lib/prompt.js +108 -0
- package/out/lib/types.d.ts +417 -0
- package/out/lib/types.js +21 -0
- package/out/lib/ui.d.ts +114 -0
- package/out/lib/ui.js +265 -0
- package/out/lib/version.d.ts +24 -0
- package/out/lib/version.js +27 -0
- package/out/lib/voice.d.ts +50 -0
- package/out/lib/voice.js +218 -0
- package/out/postinstall.d.ts +2 -0
- package/out/postinstall.js +92 -0
- package/out/thin.d.ts +2 -0
- package/out/thin.js +259 -0
- package/package.json +101 -0
- package/scripts/read_python.py +270 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.TerminalSession = void 0;
|
|
37
|
+
/**
|
|
38
|
+
* A coding job, run from a terminal.
|
|
39
|
+
*
|
|
40
|
+
* Specification: docs/modules/CODING_AGENT_BUILD_SPECIFICATION.md, register
|
|
41
|
+
* items A6 (interactive), A7 (scriptable), A10 and H6 (one job, any client),
|
|
42
|
+
* C14 (resume) and D40 (a terminal the human can watch).
|
|
43
|
+
*
|
|
44
|
+
* ## What this file is, and what it is not
|
|
45
|
+
*
|
|
46
|
+
* It is the wire between a terminal and the loop, plus the rendering. **The loop
|
|
47
|
+
* is not here.** Which model takes each step, what an action costs, whether an
|
|
48
|
+
* action is allowed and what gets written down are all decided on the server,
|
|
49
|
+
* exactly as they are for the browser. This connects, performs what it is asked,
|
|
50
|
+
* prints what happened, and asks the person when the server says to.
|
|
51
|
+
*
|
|
52
|
+
* That is deliberate and it is the whole reason a terminal agent was cheap to
|
|
53
|
+
* build at all: the second client reuses everything except the drawing.
|
|
54
|
+
*
|
|
55
|
+
* ## Two ways to run
|
|
56
|
+
*
|
|
57
|
+
* Interactive, with a person at the keyboard who can approve, interrupt and
|
|
58
|
+
* steer. And headless, driven by a script with nobody present, where an
|
|
59
|
+
* approval has no one to ask and the permission mode decides everything up
|
|
60
|
+
* front. Both are the same session; the difference is who answers.
|
|
61
|
+
*/
|
|
62
|
+
const gateway_1 = require("../lib/gateway");
|
|
63
|
+
const readline = __importStar(require("readline"));
|
|
64
|
+
const executor_1 = require("../executor");
|
|
65
|
+
const ui = __importStar(require("./render"));
|
|
66
|
+
class TerminalSession {
|
|
67
|
+
socket = null;
|
|
68
|
+
executor;
|
|
69
|
+
opts;
|
|
70
|
+
out;
|
|
71
|
+
lastTodos = "";
|
|
72
|
+
/** The last thing it said on screen, so the ending does not say it twice.
|
|
73
|
+
* Every step prints what the model said, and the closing frame carries the
|
|
74
|
+
* final text again, so the answer to a one step job appeared twice in a row
|
|
75
|
+
* with nothing between the copies. */
|
|
76
|
+
lastSaid = "";
|
|
77
|
+
canceled = false;
|
|
78
|
+
constructor(opts, write = (s) => process.stdout.write(s)) {
|
|
79
|
+
this.opts = opts;
|
|
80
|
+
this.out = write;
|
|
81
|
+
this.executor = new executor_1.NativeExecutor({
|
|
82
|
+
root: opts.root,
|
|
83
|
+
// Streamed straight through while a command runs. A build that takes two
|
|
84
|
+
// minutes must not look like a hang, and the output is the only thing
|
|
85
|
+
// that proves it is not.
|
|
86
|
+
onOutput: (chunk) => {
|
|
87
|
+
if (!opts.headless)
|
|
88
|
+
this.out(ui.gray(chunk));
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/** Where the socket lives, derived from the workspace URL.
|
|
93
|
+
*
|
|
94
|
+
* Derived rather than configured: a second setting for the socket address is
|
|
95
|
+
* a second thing that can point at the wrong server, and the failure would be
|
|
96
|
+
* a job running against somebody else's workspace. */
|
|
97
|
+
/**
|
|
98
|
+
* A pass for this job's stream, from the one service.
|
|
99
|
+
*
|
|
100
|
+
* The job is authorized THERE, where everything else in the product is
|
|
101
|
+
* authorized, and this returns only permission to open the stream. Null when
|
|
102
|
+
* the service refused or could not be reached, and the caller then reports
|
|
103
|
+
* the service's own words rather than guessing at them.
|
|
104
|
+
*/
|
|
105
|
+
async pass(request) {
|
|
106
|
+
const reply = await (0, gateway_1.ask)({
|
|
107
|
+
intent: "job_start",
|
|
108
|
+
prompt: request,
|
|
109
|
+
...(this.opts.resumeJobId ? { resumeJobId: this.opts.resumeJobId } : {}),
|
|
110
|
+
});
|
|
111
|
+
// An OLDER WORKSPACE that has never heard of this question is not a
|
|
112
|
+
// refusal. Its own credential path is alive and works, so the job falls
|
|
113
|
+
// back to it rather than stopping. This is the one place in the design
|
|
114
|
+
// where a second path is permitted, it is temporary, and it is deleted in
|
|
115
|
+
// the release after every workspace is known to be current.
|
|
116
|
+
if (reply.served === false) {
|
|
117
|
+
// Not a refusal. The workspace's own credential path is alive and works,
|
|
118
|
+
// so the job proceeds on it rather than stopping. This is the one place
|
|
119
|
+
// in the design where a second path is permitted, it is temporary, and it
|
|
120
|
+
// goes in the release after every workspace is known to be current.
|
|
121
|
+
this.olderWorkspace = true;
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if (!reply.ok || !reply.job?.ticket) {
|
|
125
|
+
// A real refusal, in the service's own words: a plan that does not
|
|
126
|
+
// include coding and an allowance that has been spent have different
|
|
127
|
+
// fixes, and a caller told only "refused" looks in the wrong place.
|
|
128
|
+
this.refusal = reply.message || "your workspace could not be reached";
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return { ticket: reply.job.ticket, jobId: reply.job.job_id };
|
|
132
|
+
}
|
|
133
|
+
/** Where the stream lives, derived from the workspace URL.
|
|
134
|
+
*
|
|
135
|
+
* Derived rather than configured: a second setting for the stream address is
|
|
136
|
+
* a second thing that can point at the wrong server, and the failure would be
|
|
137
|
+
* a job running against somebody else's workspace.
|
|
138
|
+
*
|
|
139
|
+
* The credential still travels because the pass is redeemed against the
|
|
140
|
+
* tenant it was issued in, and the pass is what actually authorizes the job.
|
|
141
|
+
* Without a pass this falls back to the old behavior, which is what a
|
|
142
|
+
* workspace running an older build still understands. */
|
|
143
|
+
socketUrl(ticket) {
|
|
144
|
+
const url = new URL(this.opts.baseUrl);
|
|
145
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
146
|
+
url.pathname = "/api/v1/agent/ws";
|
|
147
|
+
url.search = `?token=${encodeURIComponent(this.opts.credential)}`
|
|
148
|
+
+ (ticket ? `&ticket=${encodeURIComponent(ticket)}` : "");
|
|
149
|
+
return url.toString();
|
|
150
|
+
}
|
|
151
|
+
/** The name the map is held under: the folder's own, which is exactly what
|
|
152
|
+
* the run frame already sends as the workspace. One expression, so the map
|
|
153
|
+
* and the job record can never be about two different projects. */
|
|
154
|
+
project() {
|
|
155
|
+
return this.opts.root.split("/").filter(Boolean).pop() || this.opts.root;
|
|
156
|
+
}
|
|
157
|
+
/** Why the one service would not start this job, in its own words. */
|
|
158
|
+
refusal = null;
|
|
159
|
+
/** The workspace does not carry the permission question yet, so fall back. */
|
|
160
|
+
olderWorkspace = false;
|
|
161
|
+
/** Set while one is actually running, so two never overlap. */
|
|
162
|
+
remapping = false;
|
|
163
|
+
/**
|
|
164
|
+
* The agent just changed something on disk. Bring the map back in step.
|
|
165
|
+
*
|
|
166
|
+
* Only for actions that can change a file. A read or a search leaves the
|
|
167
|
+
* project exactly as it was, and re-reading it would be a round trip bought
|
|
168
|
+
* for nothing on the most common actions there are.
|
|
169
|
+
*
|
|
170
|
+
* The refresh itself is the ordinary one: it compares the files against what
|
|
171
|
+
* the service holds and sends back only what moved, which after one edit is
|
|
172
|
+
* one file. Debounced by a short wait, because an agent that writes four
|
|
173
|
+
* files in one step should pay for one update, not four.
|
|
174
|
+
*/
|
|
175
|
+
async noteChanged(tool, input) {
|
|
176
|
+
if (this.opts.withoutMap)
|
|
177
|
+
return;
|
|
178
|
+
// A named file changed, so exactly that file is put back into the map and
|
|
179
|
+
// the result waits for it. One round trip, and the agent can ask about what
|
|
180
|
+
// it just wrote in the very same step.
|
|
181
|
+
const NAMES_ITS_FILE = new Set([
|
|
182
|
+
"write_file", "edit_file", "multi_edit", "delete_file", "move_file",
|
|
183
|
+
"write_bytes", "create_dir",
|
|
184
|
+
]);
|
|
185
|
+
if (NAMES_ITS_FILE.has(tool)) {
|
|
186
|
+
const a = (input ?? {});
|
|
187
|
+
const paths = [a.path, a.file_path, a.from, a.to, a.destination]
|
|
188
|
+
.filter((p) => typeof p === "string" && p.length > 0);
|
|
189
|
+
const edits = Array.isArray(a.edits) ? a.edits : [];
|
|
190
|
+
for (const e of edits) {
|
|
191
|
+
const one = e?.path;
|
|
192
|
+
if (typeof one === "string")
|
|
193
|
+
paths.push(one);
|
|
194
|
+
}
|
|
195
|
+
if (paths.length) {
|
|
196
|
+
const { pushFile } = await Promise.resolve().then(() => __importStar(require("../graph/sync")));
|
|
197
|
+
await pushFile(this.opts.root, this.project(), [...new Set(paths)]);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// A command, a patch or an install can change anything: a formatter, a code
|
|
202
|
+
// generator, a package manager writing a lockfile. Nothing here knows what
|
|
203
|
+
// it touched, so the whole project is re-checked rather than one file. That
|
|
204
|
+
// is the comparison that costs milliseconds, and only the files that
|
|
205
|
+
// actually moved are read.
|
|
206
|
+
const CHANGES_SOMETHING = new Set([
|
|
207
|
+
"apply_patch", "run_command", "run_background", "install_dependency",
|
|
208
|
+
"run_tests", "git_branch", "git_commit",
|
|
209
|
+
]);
|
|
210
|
+
if (CHANGES_SOMETHING.has(tool))
|
|
211
|
+
await this.remap();
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Put the files this job changed back into the map.
|
|
215
|
+
*
|
|
216
|
+
* Runs after the job, without being waited on: the person has their answer
|
|
217
|
+
* and a re-read of three files must not hold up their prompt. A failure here
|
|
218
|
+
* costs nothing, because the next job checks the files anyway and would read
|
|
219
|
+
* them then.
|
|
220
|
+
*/
|
|
221
|
+
async remap() {
|
|
222
|
+
if (this.opts.withoutMap || this.remapping)
|
|
223
|
+
return;
|
|
224
|
+
this.remapping = true;
|
|
225
|
+
try {
|
|
226
|
+
const { refresh } = await Promise.resolve().then(() => __importStar(require("../graph/sync")));
|
|
227
|
+
await refresh(this.opts.root, this.project());
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
/* the next check picks the files up anyway */
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
this.remapping = false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/** Run one request to its end. Resolves with how it went. */
|
|
237
|
+
async run(request) {
|
|
238
|
+
const result = {
|
|
239
|
+
outcome: "failed", steps: 0, actions: 0, totalUsd: 0, savedUsd: null,
|
|
240
|
+
receipt: "", models: [], text: "",
|
|
241
|
+
};
|
|
242
|
+
// ── the map of this project, brought up to date first ──────────────────
|
|
243
|
+
//
|
|
244
|
+
// Before the job, not after it, because the whole saving is the agent
|
|
245
|
+
// knowing the shape of the codebase instead of reading forty files to work
|
|
246
|
+
// it out. A map read afterwards would have cost the money it exists to
|
|
247
|
+
// save.
|
|
248
|
+
//
|
|
249
|
+
// Only what MOVED is read. An unchanged project costs about ten
|
|
250
|
+
// milliseconds here, which is the check itself; a few changed files cost a
|
|
251
|
+
// fraction of a second. Nothing about this can stop the job: a service that
|
|
252
|
+
// cannot be reached, a company that has switched the map off, and a machine
|
|
253
|
+
// with no parser all end the same way, with the agent reading files the
|
|
254
|
+
// way every other coding tool does all of the time.
|
|
255
|
+
if (!this.opts.withoutMap) {
|
|
256
|
+
try {
|
|
257
|
+
const { refresh, status } = await Promise.resolve().then(() => __importStar(require("../graph/sync")));
|
|
258
|
+
const now = await status(this.opts.root, this.project());
|
|
259
|
+
if (now.enabled && !now.hasMap) {
|
|
260
|
+
// FIRST TIME IN THIS PROJECT: read it, but do not make anybody wait.
|
|
261
|
+
//
|
|
262
|
+
// A first read of a large project is minutes, because every file has
|
|
263
|
+
// to be parsed and its structure sent. Waiting for that before the
|
|
264
|
+
// job starts would make somebody's first experience of the product a
|
|
265
|
+
// long pause for a feature they did not ask for, to save money on a
|
|
266
|
+
// job they have not started. So the job runs now, reading files the
|
|
267
|
+
// ordinary way, and the second job in this project has the map.
|
|
268
|
+
this.out(ui.dim(" reading a map of this project in the background;"
|
|
269
|
+
+ " your next job here will use it\n"));
|
|
270
|
+
void refresh(this.opts.root, this.project()).catch(() => undefined);
|
|
271
|
+
}
|
|
272
|
+
else if (now.enabled && !now.fresh) {
|
|
273
|
+
// A map already exists and some files moved. Only those are read,
|
|
274
|
+
// which is a fraction of a second, and it is worth waiting for:
|
|
275
|
+
// answering this job from a map that is behind the files is the one
|
|
276
|
+
// failure this whole feature has to avoid.
|
|
277
|
+
const done = await refresh(this.opts.root, this.project());
|
|
278
|
+
if (done.filesRead)
|
|
279
|
+
this.out(ui.dim(` code map: ${done.message}\n`));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
/* the map is an optimization; never a reason a job does not start */
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Permission first, from the one service. The stream opens only once the
|
|
287
|
+
// job has been authorized there, so this socket carries no decision of its
|
|
288
|
+
// own and the workspace credential is no longer the thing that opens it.
|
|
289
|
+
const granted = await this.pass(request);
|
|
290
|
+
if (this.olderWorkspace) {
|
|
291
|
+
// Straight on to the stream with the credential, silently. A person does
|
|
292
|
+
// not need to be told their workspace is a release behind when the thing
|
|
293
|
+
// they asked for is about to happen anyway.
|
|
294
|
+
this.refusal = null;
|
|
295
|
+
}
|
|
296
|
+
else if (!granted && this.refusal) {
|
|
297
|
+
// A refusal is an ANSWER, not a failure to connect. It carries the
|
|
298
|
+
// service's own wording, because a plan that does not include coding and
|
|
299
|
+
// an allowance that has been spent have different fixes.
|
|
300
|
+
result.detail = this.refusal;
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
await new Promise((resolve, reject) => {
|
|
304
|
+
const socket = new WebSocket(this.socketUrl(granted?.ticket));
|
|
305
|
+
this.socket = socket;
|
|
306
|
+
let ready = false;
|
|
307
|
+
socket.onopen = () => {
|
|
308
|
+
socket.send(JSON.stringify({
|
|
309
|
+
type: "hello",
|
|
310
|
+
client: this.opts.headless ? "cli-headless" : "cli",
|
|
311
|
+
capabilities: this.executor.capabilities,
|
|
312
|
+
}));
|
|
313
|
+
};
|
|
314
|
+
socket.onmessage = async (event) => {
|
|
315
|
+
let frame;
|
|
316
|
+
try {
|
|
317
|
+
frame = JSON.parse(String(event.data));
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (frame.type === "ready" && !ready) {
|
|
323
|
+
ready = true;
|
|
324
|
+
this.announce(frame);
|
|
325
|
+
socket.send(JSON.stringify({
|
|
326
|
+
type: this.opts.resumeJobId ? "resume" : "run",
|
|
327
|
+
job_id: this.opts.resumeJobId,
|
|
328
|
+
continues_job: this.opts.continuesJob,
|
|
329
|
+
request,
|
|
330
|
+
workspace: this.opts.root.split("/").pop(),
|
|
331
|
+
workspace_source: "local",
|
|
332
|
+
workspace_root: this.opts.root,
|
|
333
|
+
client: this.opts.headless ? "cli-headless" : "cli",
|
|
334
|
+
permission_mode: this.opts.permissionMode ?? "approve_writes",
|
|
335
|
+
plan_only: Boolean(this.opts.planOnly),
|
|
336
|
+
plan_approved: Boolean(this.opts.planApproved),
|
|
337
|
+
model: this.opts.model,
|
|
338
|
+
pin_model: Boolean(this.opts.pinModel),
|
|
339
|
+
max_spend_usd: this.opts.maxSpendUsd,
|
|
340
|
+
approved_commands: this.opts.approvedCommands ?? [],
|
|
341
|
+
attachments: this.opts.attachmentIds ?? [],
|
|
342
|
+
without_graph: Boolean(this.opts.withoutMap),
|
|
343
|
+
}));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (frame.type === "tool_call") {
|
|
347
|
+
const r = await this.executor.execute(String(frame.name), frame.input ?? {});
|
|
348
|
+
// THE MAP IS CAUGHT UP BEFORE THE RESULT GOES BACK.
|
|
349
|
+
//
|
|
350
|
+
// Before, not after, and that ordering is the whole of it. The server
|
|
351
|
+
// runs a step's actions one after another, so an agent that writes a
|
|
352
|
+
// file and then asks the map about it does both inside one step. A
|
|
353
|
+
// refresh started after this result is sent has not finished when the
|
|
354
|
+
// question runs, and the agent is told the thing it just wrote does
|
|
355
|
+
// not exist. Measured on a real job before this line existed.
|
|
356
|
+
if (!r.isError)
|
|
357
|
+
await this.noteChanged(String(frame.name), frame.input);
|
|
358
|
+
socket.send(JSON.stringify({
|
|
359
|
+
type: "tool_result", id: frame.id,
|
|
360
|
+
content: r.content, is_error: Boolean(r.isError), summary: r.summary,
|
|
361
|
+
}));
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (frame.type === "approval_ask") {
|
|
365
|
+
const granted = await this.ask(String(frame.name), frame.input ?? {});
|
|
366
|
+
socket.send(JSON.stringify({ type: "approval_reply", id: frame.id, granted }));
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (frame.type === "event") {
|
|
370
|
+
this.narrate(frame);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (frame.type === "done") {
|
|
374
|
+
result.outcome = String(frame.outcome ?? "failed");
|
|
375
|
+
result.detail = frame.detail ?? null;
|
|
376
|
+
result.jobId = frame.job_id;
|
|
377
|
+
result.steps = Number(frame.steps ?? 0);
|
|
378
|
+
result.actions = Number(frame.actions ?? 0);
|
|
379
|
+
result.totalUsd = Number(frame.total_usd ?? 0);
|
|
380
|
+
// The saving is the server's arithmetic, taken as it is sent. A
|
|
381
|
+
// client that subtracted its own would disagree with the stored
|
|
382
|
+
// receipt within a release, and the stored one is the record.
|
|
383
|
+
{
|
|
384
|
+
const economics = frame.economics;
|
|
385
|
+
const saved = economics?.saved_usd;
|
|
386
|
+
result.savedUsd = typeof saved === "number" ? saved : null;
|
|
387
|
+
const rows = economics?.models;
|
|
388
|
+
result.models = Array.isArray(rows) ? rows : [];
|
|
389
|
+
}
|
|
390
|
+
// The sentence the server wrote. Everything a reader is told about
|
|
391
|
+
// cost and saving comes from here.
|
|
392
|
+
result.receipt = String(frame.receipt ?? "");
|
|
393
|
+
result.reusedPct = ui.reusePct(0, frame.cached_tokens, Math.max(0, Number(frame.context_tokens ?? 0) - Number(frame.cached_tokens ?? 0)));
|
|
394
|
+
result.text = String(frame.text ?? "");
|
|
395
|
+
result.testsPassed = this.executor.lastTestsPassed;
|
|
396
|
+
// What the agent itself changed goes back into the map before the
|
|
397
|
+
// next job asks anything. Without this the second job of a session
|
|
398
|
+
// works from a picture of the code as it was before the first job
|
|
399
|
+
// edited it, which is the exact failure the freshness rule exists to
|
|
400
|
+
// prevent, and the most likely one: the agent that made the map stale
|
|
401
|
+
// is the agent about to trust it.
|
|
402
|
+
void this.remap();
|
|
403
|
+
socket.close();
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (frame.type === "error") {
|
|
407
|
+
this.out(ui.red(`\n ${String(frame.message ?? "Something went wrong.")}\n`));
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
socket.onerror = () => {
|
|
411
|
+
// Say what actually failed.
|
|
412
|
+
//
|
|
413
|
+
// This used to read "Check you are signed in", on a screen that had
|
|
414
|
+
// just printed the person's own email, their company and their plan,
|
|
415
|
+
// all of which came from the very server it claimed it could not reach.
|
|
416
|
+
// Being told to do the one thing you have visibly already done sends
|
|
417
|
+
// somebody to sign in again, which fixes nothing and costs an evening.
|
|
418
|
+
//
|
|
419
|
+
// By the time this fires the credential has already been accepted: the
|
|
420
|
+
// job was authorized and the server issued a pass for it. What failed
|
|
421
|
+
// is the live connection that carries the work, and the wording names
|
|
422
|
+
// that instead.
|
|
423
|
+
if (!ready) {
|
|
424
|
+
reject(new Error("The live connection to your workspace could not be opened. "
|
|
425
|
+
+ "You are signed in; the work stream is what failed."));
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
socket.onclose = () => {
|
|
429
|
+
(0, executor_1.stopEverything)();
|
|
430
|
+
resolve();
|
|
431
|
+
};
|
|
432
|
+
});
|
|
433
|
+
this.report(result);
|
|
434
|
+
return result;
|
|
435
|
+
}
|
|
436
|
+
/** Stop the running job. Takes effect mid action, not at the end of the run. */
|
|
437
|
+
cancel() {
|
|
438
|
+
if (this.canceled)
|
|
439
|
+
return;
|
|
440
|
+
this.canceled = true;
|
|
441
|
+
this.out(ui.yellow("\n stopping...\n"));
|
|
442
|
+
this.socket?.send(JSON.stringify({ type: "cancel" }));
|
|
443
|
+
}
|
|
444
|
+
/** Something typed while it works. Joins the conversation at the next step. */
|
|
445
|
+
steer(text) {
|
|
446
|
+
this.socket?.send(JSON.stringify({ type: "steer", text }));
|
|
447
|
+
this.out(ui.dim(" noted, it will pick that up on the next step\n"));
|
|
448
|
+
}
|
|
449
|
+
announce(frame) {
|
|
450
|
+
const allowance = frame.allowance;
|
|
451
|
+
const tools = frame.tools ?? [];
|
|
452
|
+
this.out(ui.dim(` ${this.opts.root} ${tools.length} actions available\n`));
|
|
453
|
+
if (allowance && allowance.included && !allowance.unlimited) {
|
|
454
|
+
this.out(ui.dim(` coding allowance left this month: ${ui.money(Number(allowance.remaining_usd))}\n`));
|
|
455
|
+
}
|
|
456
|
+
if (this.opts.planOnly)
|
|
457
|
+
this.out(ui.yellow(" plan only: nothing will be changed\n"));
|
|
458
|
+
if (this.opts.planApproved) {
|
|
459
|
+
this.out(ui.dim(" plan approved: file changes will not be asked about one at a time\n"));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
narrate(frame) {
|
|
463
|
+
switch (frame.kind) {
|
|
464
|
+
case "step_finished": {
|
|
465
|
+
this.out(ui.step(Number(frame.index ?? 0), String(frame.model ?? ""), frame.usd, frame.latency_ms, ui.reusePct(frame.tokens_in, frame.cache_read_tokens, frame.cache_write_tokens)) + "\n");
|
|
466
|
+
const said = String(frame.text ?? "").trim();
|
|
467
|
+
// Markdown is for a browser. A terminal shows the asterisks.
|
|
468
|
+
if (said) {
|
|
469
|
+
this.out(`\n${ui.plain(said)}\n`);
|
|
470
|
+
this.lastSaid = said;
|
|
471
|
+
}
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
case "action_finished":
|
|
475
|
+
this.out(ui.action(String(frame.name ?? ""), this.targetOf(frame), frame.ok !== false, frame.latency_ms) + "\n");
|
|
476
|
+
break;
|
|
477
|
+
case "action_refused":
|
|
478
|
+
this.out(ui.red(` refused ${String(frame.name ?? "")}`)
|
|
479
|
+
+ ui.dim(` ${String(frame.reason ?? "")}\n`));
|
|
480
|
+
break;
|
|
481
|
+
case "todos": {
|
|
482
|
+
const rendered = ui.todos(frame.todos ?? []);
|
|
483
|
+
// Only when it has actually changed. A plan reprinted identically after
|
|
484
|
+
// every step is noise that buries the run.
|
|
485
|
+
if (rendered !== this.lastTodos) {
|
|
486
|
+
this.out(rendered);
|
|
487
|
+
this.lastTodos = rendered;
|
|
488
|
+
}
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
// A file the agent produced, and where it went. Said here rather than
|
|
492
|
+
// left to the model's sentence: a terminal has no timeline to look at, and
|
|
493
|
+
// "ready to download" in a terminal is the same as producing nothing.
|
|
494
|
+
case "document_made": {
|
|
495
|
+
const saved = frame.saved_to ? String(frame.saved_to) : "";
|
|
496
|
+
const bytes = Number(frame.bytes ?? 0).toLocaleString();
|
|
497
|
+
this.out(saved
|
|
498
|
+
? ui.green(` saved ${saved}`) + ui.dim(` ${bytes} bytes\n`)
|
|
499
|
+
: ui.yellow(` made ${String(frame.filename ?? "a file")}`)
|
|
500
|
+
+ ui.dim(` ${bytes} bytes, not saved here\n`));
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
case "compacting":
|
|
504
|
+
this.out(ui.dim(" summarizing the conversation so far to keep it affordable\n"));
|
|
505
|
+
break;
|
|
506
|
+
case "steered":
|
|
507
|
+
break;
|
|
508
|
+
default:
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
targetOf(frame) {
|
|
513
|
+
const input = frame.input ?? {};
|
|
514
|
+
return String(input.path ?? input.pattern ?? input.command ?? input.to ?? "");
|
|
515
|
+
}
|
|
516
|
+
/** Put a change in front of the person and wait.
|
|
517
|
+
*
|
|
518
|
+
* Three cases, in order. A caller that owns the keyboard lent us one, so the
|
|
519
|
+
* question goes there. Nobody lent one and somebody is watching, so we open
|
|
520
|
+
* our own reader. Nobody is watching, so it is refused.
|
|
521
|
+
*
|
|
522
|
+
* In headless mode there is nobody to ask, so it is refused rather than
|
|
523
|
+
* assumed. Treating silence as consent would let a script approve changes
|
|
524
|
+
* nobody ever saw, which is the opposite of what a permission mode is for:
|
|
525
|
+
* a script that needs to change files says so by choosing a mode that allows
|
|
526
|
+
* it, up front. */
|
|
527
|
+
async ask(name, input) {
|
|
528
|
+
if (this.opts.headless) {
|
|
529
|
+
// Name what was refused. A refusal that does not say WHICH command it
|
|
530
|
+
// turned down cannot be acted on: the operator's only remedy is to
|
|
531
|
+
// pre-approve it with --allow, and they cannot approve a string they were
|
|
532
|
+
// never shown. Found while running the same job unattended five ways and
|
|
533
|
+
// watching every arm stall on a command none of them would name.
|
|
534
|
+
const what = this.targetOf({ input });
|
|
535
|
+
this.out(ui.yellow(` ${name} needs approval and nobody is watching, so it was refused. `)
|
|
536
|
+
+ (what ? ui.dim(`Refused: ${what}\n `) : "")
|
|
537
|
+
+ ui.dim("Re-run with --allow \"<that exact command>\", or choose a permission mode that allows it.\n"));
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
this.out(ui.diff(name, input));
|
|
541
|
+
if (this.opts.confirm) {
|
|
542
|
+
const granted = await this.opts.confirm(`${ui.bold("Apply this?")} ${ui.dim("[y/N]")}`);
|
|
543
|
+
this.out(granted ? ui.green(" applied\n") : ui.dim(" skipped\n"));
|
|
544
|
+
return granted;
|
|
545
|
+
}
|
|
546
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
547
|
+
try {
|
|
548
|
+
const answer = await new Promise((resolve) => {
|
|
549
|
+
rl.question(` ${ui.bold("Apply this?")} ${ui.dim("[y/N]")} `, resolve);
|
|
550
|
+
});
|
|
551
|
+
const granted = /^y(es)?$/i.test(answer.trim());
|
|
552
|
+
this.out(granted ? ui.green(" applied\n") : ui.dim(" skipped\n"));
|
|
553
|
+
return granted;
|
|
554
|
+
}
|
|
555
|
+
finally {
|
|
556
|
+
rl.close();
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
report(r) {
|
|
560
|
+
// Only if it has not already been said. On a one step job the closing text
|
|
561
|
+
// IS the step's text, and printing it again read as a stutter.
|
|
562
|
+
if (r.text && r.text.trim() !== this.lastSaid)
|
|
563
|
+
this.out(`\n${ui.plain(r.text)}\n`);
|
|
564
|
+
this.out(ui.outcome(r.outcome, r.detail));
|
|
565
|
+
this.out(ui.receipt(r.receipt, r.models, r.testsPassed));
|
|
566
|
+
if (r.jobId) {
|
|
567
|
+
// The job's own reference, so a person can quote it. The link to the
|
|
568
|
+
// account sits in the receipt above, where the server put it.
|
|
569
|
+
this.out(ui.dim(` job ${r.jobId}\n`));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
exports.TerminalSession = TerminalSession;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cmdAsk(args: string[]): Promise<void>;
|