@qilin-agent/commands 0.0.0-stage → 3.1.3
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 +21 -0
- package/README.i18n.yaml +76 -0
- package/README.md +148 -2
- package/README.zh.md +149 -0
- package/lib/index.js +478 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +821 -0
- package/lib/typert.remote-client.d.ts +29 -0
- package/lib/typert.remote-client.js +131 -0
- package/lib/types/brand.d.ts +32 -0
- package/lib/types/brand.js +27 -0
- package/lib/types/index.d.ts +160 -0
- package/lib/types/index.js +462 -0
- package/lib/types/types.d.ts +121 -0
- package/lib/types/types.js +10 -0
- package/package.json +74 -3
package/lib/index.js
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import { randomUUID } from "@qilin-agent/util-crypto";
|
|
2
|
+
import { AttachmentError, admitEncodedImages } from "@qilin-agent/attachment";
|
|
3
|
+
import { NamedEntries, ScopedLayers } from "@qilin-agent/scope";
|
|
4
|
+
import { SessionSeq } from "@qilin-agent/session";
|
|
5
|
+
import { Remote, TypertRemoteService } from "@qilin-agent/typert-protocol";
|
|
6
|
+
//#region lib/types/brand.js
|
|
7
|
+
/**
|
|
8
|
+
* Command definition identities and execution ids for discovery and lifecycle pairing.
|
|
9
|
+
*
|
|
10
|
+
* The `Branded<B>` primitive lives in `@qilin-agent/brand`; this module
|
|
11
|
+
* is a pure type/constructor outlet (no cordis imports, no module
|
|
12
|
+
* augmentation) so wire and client programs can name the brand without
|
|
13
|
+
* loading the host plugin's Context merges — the `qilin-llm/brand` shape.
|
|
14
|
+
*
|
|
15
|
+
* @module @qilin-agent/commands/brand
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Brand a plugin-namespaced command definition identity.
|
|
19
|
+
* @param id - stable identity chosen by the registering plugin.
|
|
20
|
+
* @returns the same string, branded; no validation is performed.
|
|
21
|
+
*/
|
|
22
|
+
function CommandDefinitionId(id) {
|
|
23
|
+
return id;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Brand a string as a {@link CommandId}.
|
|
27
|
+
* @param id - the executor-minted pairing id.
|
|
28
|
+
* @returns the same string, branded; no validation is performed.
|
|
29
|
+
*/
|
|
30
|
+
function CommandId(id) {
|
|
31
|
+
return id;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region lib/types/index.js
|
|
35
|
+
/**
|
|
36
|
+
* Plugin-owned human-command registry shared by interactive UI adapters.
|
|
37
|
+
* @module @qilin-agent/commands
|
|
38
|
+
*/
|
|
39
|
+
var __runInitializers = function(thisArg, initializers, value) {
|
|
40
|
+
var useValue = arguments.length > 2;
|
|
41
|
+
for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
42
|
+
return useValue ? value : void 0;
|
|
43
|
+
};
|
|
44
|
+
var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
45
|
+
function accept(f) {
|
|
46
|
+
if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
|
|
47
|
+
return f;
|
|
48
|
+
}
|
|
49
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
50
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
51
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
52
|
+
var _, done = false;
|
|
53
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
54
|
+
var context = {};
|
|
55
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
56
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
57
|
+
context.addInitializer = function(f) {
|
|
58
|
+
if (done) throw new TypeError("Cannot add initializers after decoration has completed");
|
|
59
|
+
extraInitializers.push(accept(f || null));
|
|
60
|
+
};
|
|
61
|
+
var result = (0, decorators[i])(kind === "accessor" ? {
|
|
62
|
+
get: descriptor.get,
|
|
63
|
+
set: descriptor.set
|
|
64
|
+
} : descriptor[key], context);
|
|
65
|
+
if (kind === "accessor") {
|
|
66
|
+
if (result === void 0) continue;
|
|
67
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
68
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
69
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
70
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
71
|
+
} else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
|
|
72
|
+
else descriptor[key] = _;
|
|
73
|
+
}
|
|
74
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
75
|
+
done = true;
|
|
76
|
+
};
|
|
77
|
+
const name = "commands";
|
|
78
|
+
const COMMAND_NAME = /^[a-z][a-z0-9_-]*$/u;
|
|
79
|
+
/** Shared frozen attachments value for attachment-free invocations. */
|
|
80
|
+
const NO_ATTACHMENTS = Object.freeze([]);
|
|
81
|
+
/** All command registrations owned by one global or scoped layer. */
|
|
82
|
+
var CommandLayer = class {
|
|
83
|
+
commands;
|
|
84
|
+
/**
|
|
85
|
+
* Create one command layer with diagnostics specific to its ownership scope.
|
|
86
|
+
* @param scope - the scoped owner, or `undefined` for global registrations.
|
|
87
|
+
*/
|
|
88
|
+
constructor(scope) {
|
|
89
|
+
this.commands = new NamedEntries((name) => /* @__PURE__ */ new Error(scope === void 0 ? `command "${name}" is already registered (for a per-agent variant, mount a command-injected plugin under that agent's \`agent.ctx\`)` : `command "${name}" is already registered in this scope`));
|
|
90
|
+
}
|
|
91
|
+
/** @returns whether this layer owns no command registrations. */
|
|
92
|
+
isEmpty() {
|
|
93
|
+
return this.commands.isEmpty();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Parse an exact slash command without normalizing its trailing input.
|
|
98
|
+
*
|
|
99
|
+
* @param line - Complete candidate command line.
|
|
100
|
+
* @returns The parsed command, or `undefined` when the line is not a command.
|
|
101
|
+
*/
|
|
102
|
+
function parseCommand(line) {
|
|
103
|
+
const match = /^\/([a-z][a-z0-9_-]*)(?=$|[\t\n\r ])/u.exec(line);
|
|
104
|
+
if (match === null) return void 0;
|
|
105
|
+
const name = match[1];
|
|
106
|
+
/* v8 ignore next -- the first capture is required whenever the regular expression matches */
|
|
107
|
+
if (name === void 0) return void 0;
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
name,
|
|
110
|
+
rawInput: line.slice(match[0].length)
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** Convert arbitrary abort reasons to one stable rejected Error. */
|
|
114
|
+
function abortError(signal) {
|
|
115
|
+
if (signal.reason instanceof Error) return signal.reason;
|
|
116
|
+
return new Error(typeof signal.reason === "string" ? signal.reason : "command aborted");
|
|
117
|
+
}
|
|
118
|
+
/** The signal's normalized abort error when it is already aborted. */
|
|
119
|
+
function cancellationOf(signal) {
|
|
120
|
+
return signal.aborted ? abortError(signal) : void 0;
|
|
121
|
+
}
|
|
122
|
+
/** Render arbitrary thrown values without trusting their string coercion. */
|
|
123
|
+
function renderThrown(value) {
|
|
124
|
+
try {
|
|
125
|
+
return String(value);
|
|
126
|
+
} catch {
|
|
127
|
+
return "<unrenderable thrown value>";
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Stop awaiting an uncooperative handler once its owning UI request aborts. */
|
|
131
|
+
function withAbort(promise, signal) {
|
|
132
|
+
if (signal.aborted) return Promise.reject(abortError(signal));
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const onAbort = () => {
|
|
135
|
+
signal.removeEventListener("abort", onAbort);
|
|
136
|
+
reject(abortError(signal));
|
|
137
|
+
};
|
|
138
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
139
|
+
promise.then((value) => {
|
|
140
|
+
signal.removeEventListener("abort", onAbort);
|
|
141
|
+
resolve(value);
|
|
142
|
+
}, (error) => {
|
|
143
|
+
signal.removeEventListener("abort", onAbort);
|
|
144
|
+
reject(error instanceof Error ? error : new Error(`command handler rejected with a non-Error value: ${renderThrown(error)}`, { cause: error }));
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/** Reject invalid command metadata before it can reach a UI protocol. */
|
|
149
|
+
function normalizeDefinition(definition) {
|
|
150
|
+
if (!COMMAND_NAME.test(definition.name)) throw new TypeError(`command name "${definition.name}" must match ${String(COMMAND_NAME)}`);
|
|
151
|
+
if (typeof definition.description !== "string") throw new TypeError(`command "${definition.name}" description must be a string`);
|
|
152
|
+
if (definition.description.trim().length === 0) throw new TypeError(`command "${definition.name}" description must not be empty`);
|
|
153
|
+
if (typeof definition.handler !== "function") throw new TypeError(`command "${definition.name}" handler must be a function`);
|
|
154
|
+
const rawInput = definition.input;
|
|
155
|
+
let input;
|
|
156
|
+
if (rawInput !== void 0) {
|
|
157
|
+
if (typeof rawInput !== "object" || rawInput === null || !("hint" in rawInput) || typeof rawInput.hint !== "string") throw new TypeError(`command "${definition.name}" input hint must be a string`);
|
|
158
|
+
if (rawInput.hint.trim().length === 0) throw new TypeError(`command "${definition.name}" input hint must not be empty`);
|
|
159
|
+
if ("attachments" in rawInput && rawInput.attachments !== void 0 && typeof rawInput.attachments !== "boolean") throw new TypeError(`command "${definition.name}" input attachments flag must be a boolean`);
|
|
160
|
+
input = Object.freeze({
|
|
161
|
+
hint: rawInput.hint,
|
|
162
|
+
..."attachments" in rawInput && rawInput.attachments === true ? { attachments: true } : {}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
const normalized = Object.freeze({
|
|
166
|
+
...definition.definitionId === void 0 ? {} : { definitionId: definition.definitionId },
|
|
167
|
+
name: definition.name,
|
|
168
|
+
description: definition.description,
|
|
169
|
+
...input === void 0 ? {} : { input },
|
|
170
|
+
...definition.recordInput === void 0 ? {} : { recordInput: definition.recordInput },
|
|
171
|
+
handler: definition.handler
|
|
172
|
+
});
|
|
173
|
+
return {
|
|
174
|
+
definition: normalized,
|
|
175
|
+
descriptor: Object.freeze({
|
|
176
|
+
...normalized.definitionId === void 0 ? {} : { definitionId: normalized.definitionId },
|
|
177
|
+
name: normalized.name,
|
|
178
|
+
description: normalized.description,
|
|
179
|
+
...normalized.input === void 0 ? {} : { input: normalized.input }
|
|
180
|
+
})
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/** Validate and detach an untrusted handler result at the registry boundary. */
|
|
184
|
+
function normalizeResult(command, value) {
|
|
185
|
+
if (typeof value !== "object" || value === null || !("kind" in value)) throw new TypeError(`command "${command}" handler must return a CommandResult`);
|
|
186
|
+
const result = value;
|
|
187
|
+
if (result.kind === "success") {
|
|
188
|
+
if (result.text !== void 0 && typeof result.text !== "string") throw new TypeError(`command "${command}" success text must be a string when supplied`);
|
|
189
|
+
if (result.sourceEventSeq !== void 0 && (!Number.isSafeInteger(result.sourceEventSeq) || result.sourceEventSeq < 0 || Object.is(result.sourceEventSeq, -0))) throw new TypeError(`command "${command}" success sourceEventSeq must be a non-negative safe integer when supplied`);
|
|
190
|
+
return Object.freeze({
|
|
191
|
+
kind: "success",
|
|
192
|
+
...result.text === void 0 ? {} : { text: result.text },
|
|
193
|
+
...result.sourceEventSeq === void 0 ? {} : { sourceEventSeq: SessionSeq(result.sourceEventSeq) }
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (result.kind === "error") {
|
|
197
|
+
if (typeof result.text !== "string" || result.text.trim().length === 0) throw new TypeError(`command "${command}" error text must be a non-empty string`);
|
|
198
|
+
return Object.freeze({
|
|
199
|
+
kind: "error",
|
|
200
|
+
text: result.text
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
throw new TypeError(`command "${command}" returned unknown result kind "${String(result.kind)}"`);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Human-command registry. Plain-context definitions are global; definitions
|
|
207
|
+
* registered through a command-injected child of an agent context shadow
|
|
208
|
+
* globals for that agent.
|
|
209
|
+
*/
|
|
210
|
+
let CommandRuntime = (() => {
|
|
211
|
+
let _classSuper = TypertRemoteService;
|
|
212
|
+
let _instanceExtraInitializers = [];
|
|
213
|
+
let _list_decorators;
|
|
214
|
+
let _execute_decorators;
|
|
215
|
+
return class CommandRuntime extends _classSuper {
|
|
216
|
+
static {
|
|
217
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
218
|
+
_list_decorators = [Remote];
|
|
219
|
+
_execute_decorators = [Remote];
|
|
220
|
+
__esDecorate(this, null, _list_decorators, {
|
|
221
|
+
kind: "method",
|
|
222
|
+
name: "list",
|
|
223
|
+
static: false,
|
|
224
|
+
private: false,
|
|
225
|
+
access: {
|
|
226
|
+
has: (obj) => "list" in obj,
|
|
227
|
+
get: (obj) => obj.list
|
|
228
|
+
},
|
|
229
|
+
metadata: _metadata
|
|
230
|
+
}, null, _instanceExtraInitializers);
|
|
231
|
+
__esDecorate(this, null, _execute_decorators, {
|
|
232
|
+
kind: "method",
|
|
233
|
+
name: "execute",
|
|
234
|
+
static: false,
|
|
235
|
+
private: false,
|
|
236
|
+
access: {
|
|
237
|
+
has: (obj) => "execute" in obj,
|
|
238
|
+
get: (obj) => obj.execute
|
|
239
|
+
},
|
|
240
|
+
metadata: _metadata
|
|
241
|
+
}, null, _instanceExtraInitializers);
|
|
242
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, {
|
|
243
|
+
enumerable: true,
|
|
244
|
+
configurable: true,
|
|
245
|
+
writable: true,
|
|
246
|
+
value: _metadata
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
layers = (__runInitializers(this, _instanceExtraInitializers), new ScopedLayers((scope) => new CommandLayer(scope), () => {
|
|
250
|
+
this.notifyChange();
|
|
251
|
+
}));
|
|
252
|
+
/** Monotonic per-instance counter behind {@link mintCommandId}. */
|
|
253
|
+
commandSeq = 0;
|
|
254
|
+
/** Instance token keeping minted ids unique across process restarts over one resumed log. */
|
|
255
|
+
instanceToken = randomUUID().slice(0, 8);
|
|
256
|
+
/** Optional provider installed by the Session upload owner. */
|
|
257
|
+
fileReceipts = { resolver: void 0 };
|
|
258
|
+
constructor(ctx) {
|
|
259
|
+
super(ctx, "commands");
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Register a global or calling-agent-scoped command.
|
|
263
|
+
* @param definition - discovery metadata and direct UI handler.
|
|
264
|
+
* @returns the exact effect disposer that unregisters this definition.
|
|
265
|
+
*/
|
|
266
|
+
register(definition) {
|
|
267
|
+
const registered = normalizeDefinition(definition);
|
|
268
|
+
return this.layers.effect(this.ctx, (layer) => layer.commands.insert(registered.definition.name, registered), { label: "commands.register()" });
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Register the sole authority that resolves staged file receipts for command submissions.
|
|
272
|
+
* @param resolver - Session-aware receipt resolver.
|
|
273
|
+
* @returns disposer that removes this exact resolver.
|
|
274
|
+
*/
|
|
275
|
+
registerFileReceiptResolver(resolver) {
|
|
276
|
+
if (this.fileReceipts.resolver !== void 0) throw new Error("commands: a file receipt resolver is already registered");
|
|
277
|
+
this.fileReceipts.resolver = resolver;
|
|
278
|
+
return () => {
|
|
279
|
+
if (this.fileReceipts.resolver === resolver) this.fileReceipts.resolver = void 0;
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* List the effective immutable command descriptors for one agent.
|
|
284
|
+
* @param agent - exact receiving agent and scoped-layer key.
|
|
285
|
+
* @returns name-sorted descriptors after scoped shadowing.
|
|
286
|
+
*/
|
|
287
|
+
list(agent) {
|
|
288
|
+
return Object.freeze([...this.view(agent).values()].map((command) => command.descriptor).sort((left, right) => left.name < right.name ? -1 : 1));
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Resolve one effective command definition.
|
|
292
|
+
* @param agent - exact receiving agent and scoped-layer key.
|
|
293
|
+
* @param name - command name without a slash.
|
|
294
|
+
* @returns the scoped shadow or global definition.
|
|
295
|
+
*/
|
|
296
|
+
find(agent, name) {
|
|
297
|
+
return this.view(agent).get(name)?.definition;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Parse and execute a known command without sending it to the model.
|
|
301
|
+
*
|
|
302
|
+
* A resolved command's lifecycle is logged: `command/run` is appended
|
|
303
|
+
* before the handler is invoked and `command/done` after settlement (a
|
|
304
|
+
* thrown or aborted handler settles as `kind: 'error'`). Both are direct
|
|
305
|
+
* log-only appends — no turn wraps them, and persistence drains them at
|
|
306
|
+
* ordinary checkpoints. Admission misses (syntax or unknown name) log
|
|
307
|
+
* nothing — they never entered a handler. A `command/run` append failure
|
|
308
|
+
* fails the execution loud; a `command/done` append failure on the
|
|
309
|
+
* handler-failure path is contained so the handler's own error stays the
|
|
310
|
+
* reported failure.
|
|
311
|
+
*
|
|
312
|
+
* Attachment admission is enforced here, not in the composer: attachments sent to a
|
|
313
|
+
* command that does not declare `input.attachments`, an absent attachment store,
|
|
314
|
+
* and an exceeded image limit each settle as an error result before
|
|
315
|
+
* the handler runs. Validation rejection starts no attachment writes;
|
|
316
|
+
* a storage failure can leave only unreachable content-addressed objects
|
|
317
|
+
* for deferred collection.
|
|
318
|
+
*
|
|
319
|
+
* @param agent - exact receiving agent.
|
|
320
|
+
* @param line - complete slash-command line.
|
|
321
|
+
* @param submittedAttachments - encoded images and staged file receipts accompanying the line,
|
|
322
|
+
* in submission order; empty for a plain invocation.
|
|
323
|
+
* @param signal - cancellation signal owned by the UI request.
|
|
324
|
+
* @returns the settled execution (result + lifecycle pairing id), or
|
|
325
|
+
* `undefined` when syntax or name does not resolve.
|
|
326
|
+
*/
|
|
327
|
+
async execute(agent, line, submittedAttachments, signal) {
|
|
328
|
+
const parsed = parseCommand(line);
|
|
329
|
+
if (parsed === void 0) return void 0;
|
|
330
|
+
const command = this.view(agent).get(parsed.name);
|
|
331
|
+
if (command === void 0) return void 0;
|
|
332
|
+
if (signal.aborted) throw abortError(signal);
|
|
333
|
+
const commandId = this.mintCommandId();
|
|
334
|
+
this.appendLifecycle(agent.session, "command/run", {
|
|
335
|
+
commandId,
|
|
336
|
+
name: parsed.name,
|
|
337
|
+
...command.definition.recordInput === false ? {} : { args: parsed.rawInput },
|
|
338
|
+
source: { kind: "user" }
|
|
339
|
+
});
|
|
340
|
+
const settle = (result) => {
|
|
341
|
+
this.appendLifecycle(agent.session, "command/done", {
|
|
342
|
+
commandId,
|
|
343
|
+
kind: result.kind,
|
|
344
|
+
...result.text === void 0 ? {} : { text: result.text },
|
|
345
|
+
...result.kind === "success" && result.sourceEventSeq !== void 0 ? { sourceEventSeq: result.sourceEventSeq } : {}
|
|
346
|
+
});
|
|
347
|
+
return Object.freeze({
|
|
348
|
+
commandId,
|
|
349
|
+
result: Object.freeze(result)
|
|
350
|
+
});
|
|
351
|
+
};
|
|
352
|
+
let attachments = NO_ATTACHMENTS;
|
|
353
|
+
if (submittedAttachments.length > 0) {
|
|
354
|
+
if (command.definition.input?.attachments !== true) return settle({
|
|
355
|
+
kind: "error",
|
|
356
|
+
text: `/${parsed.name} does not accept attachments`
|
|
357
|
+
});
|
|
358
|
+
const store = this.ctx.get("attachments");
|
|
359
|
+
if (store === void 0) return settle({
|
|
360
|
+
kind: "error",
|
|
361
|
+
text: `/${parsed.name}: attachments are unavailable because no attachment store is composed`
|
|
362
|
+
});
|
|
363
|
+
try {
|
|
364
|
+
attachments = await admitCommandAttachments(store, submittedAttachments, (receiptId) => this.fileReceipts.resolver?.(agent, receiptId));
|
|
365
|
+
} catch (error) {
|
|
366
|
+
if (error instanceof AttachmentError) return settle({
|
|
367
|
+
kind: "error",
|
|
368
|
+
text: error.message
|
|
369
|
+
});
|
|
370
|
+
this.settleThrown(agent.session, parsed.name, commandId, error);
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
373
|
+
const cancelledDuringAdmission = cancellationOf(signal);
|
|
374
|
+
if (cancelledDuringAdmission !== void 0) {
|
|
375
|
+
this.settleThrown(agent.session, parsed.name, commandId, cancelledDuringAdmission);
|
|
376
|
+
throw cancelledDuringAdmission;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const invocation = Object.freeze({
|
|
380
|
+
commandId,
|
|
381
|
+
agent,
|
|
382
|
+
rawInput: parsed.rawInput,
|
|
383
|
+
attachments,
|
|
384
|
+
signal
|
|
385
|
+
});
|
|
386
|
+
let result;
|
|
387
|
+
try {
|
|
388
|
+
const output = command.definition.handler(invocation);
|
|
389
|
+
result = normalizeResult(parsed.name, await withAbort(Promise.resolve(output), signal));
|
|
390
|
+
} catch (error) {
|
|
391
|
+
this.settleThrown(agent.session, parsed.name, commandId, error);
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
return settle(result);
|
|
395
|
+
}
|
|
396
|
+
/** Contained `command/done` error append for a thrown handler or admission failure. */
|
|
397
|
+
settleThrown(session, command, commandId, error) {
|
|
398
|
+
try {
|
|
399
|
+
this.appendLifecycle(session, "command/done", {
|
|
400
|
+
commandId,
|
|
401
|
+
kind: "error",
|
|
402
|
+
text: error instanceof Error ? error.message : renderThrown(error)
|
|
403
|
+
});
|
|
404
|
+
} catch (appendError) {
|
|
405
|
+
this.ctx.logger.warn(`command "${command}": command/done append failed: ${renderThrown(appendError)}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/** Mint the next pairing id (monotonic; instance-token-prefixed so a resumed log never repeats one). */
|
|
409
|
+
mintCommandId() {
|
|
410
|
+
this.commandSeq += 1;
|
|
411
|
+
return CommandId(`cmd-${this.instanceToken}-${this.commandSeq}`);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Append one log-only lifecycle event directly: no turn is opened for it and
|
|
415
|
+
* no flush is forced — persistence observes the eager `session/event` path
|
|
416
|
+
* and drains at ordinary checkpoints and teardown, like every other
|
|
417
|
+
* standalone plugin event.
|
|
418
|
+
*/
|
|
419
|
+
appendLifecycle(session, type, data) {
|
|
420
|
+
return session.append.bind(session)(type, data);
|
|
421
|
+
}
|
|
422
|
+
/** Resolve global definitions followed by exact scoped shadows. */
|
|
423
|
+
view(agent) {
|
|
424
|
+
return this.layers.merge(agent, (layer) => layer.commands);
|
|
425
|
+
}
|
|
426
|
+
/** Notify every registry observer without making UI refresh load-bearing. */
|
|
427
|
+
notifyChange() {
|
|
428
|
+
for (const callback of this.ctx.events.dispatch("emit", ["commands/change"])) try {
|
|
429
|
+
const returned = callback();
|
|
430
|
+
Promise.resolve(returned).catch((error) => {
|
|
431
|
+
this.ctx.logger.warn(`commands/change listener rejected: ${renderThrown(error)}`);
|
|
432
|
+
});
|
|
433
|
+
} catch (error) {
|
|
434
|
+
this.ctx.logger.warn(`commands/change listener threw: ${renderThrown(error)}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
})();
|
|
439
|
+
/** Admit a mixed command batch and restore its original image/file order. */
|
|
440
|
+
async function admitCommandAttachments(store, attachments, resolveFileReceipt) {
|
|
441
|
+
const files = /* @__PURE__ */ new Map();
|
|
442
|
+
for (const attachment of attachments) {
|
|
443
|
+
if (attachment.type !== "file" || files.has(attachment.receiptId)) continue;
|
|
444
|
+
const file = resolveFileReceipt(attachment.receiptId);
|
|
445
|
+
if (file === void 0) throw new AttachmentError("File upload receipt is unknown for this session.", "ATTACHMENT_NOT_FOUND");
|
|
446
|
+
files.set(attachment.receiptId, file);
|
|
447
|
+
}
|
|
448
|
+
const images = [];
|
|
449
|
+
for (const attachment of attachments) {
|
|
450
|
+
if (attachment.type !== "image") continue;
|
|
451
|
+
images.push({
|
|
452
|
+
mediaType: attachment.mediaType,
|
|
453
|
+
data: attachment.data,
|
|
454
|
+
...attachment.name === void 0 ? {} : { name: attachment.name }
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
const imageRefs = images.length === 0 ? [] : await admitEncodedImages(store, images);
|
|
458
|
+
let imageIndex = 0;
|
|
459
|
+
const blocks = [];
|
|
460
|
+
for (const attachment of attachments) {
|
|
461
|
+
if (attachment.type === "image") {
|
|
462
|
+
const ref = imageRefs[imageIndex];
|
|
463
|
+
imageIndex += 1;
|
|
464
|
+
blocks.push(Object.freeze({
|
|
465
|
+
type: "image",
|
|
466
|
+
attachment: ref
|
|
467
|
+
}));
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
blocks.push(Object.freeze({
|
|
471
|
+
type: "file",
|
|
472
|
+
attachment: files.get(attachment.receiptId)
|
|
473
|
+
}));
|
|
474
|
+
}
|
|
475
|
+
return Object.freeze(blocks);
|
|
476
|
+
}
|
|
477
|
+
//#endregion
|
|
478
|
+
export { CommandDefinitionId, CommandId, CommandRuntime, CommandRuntime as default, name, parseCommand };
|