@khorsheed/dsh-local-agent-dsh-headless 0.1.0-rc.6

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/lib/index.js ADDED
@@ -0,0 +1,507 @@
1
+ import { LIVE_SERVER_NAME, LIVE_WIRE_ERROR_INTERNAL, LIVE_WIRE_ERROR_METHOD_NOT_FOUND } from "./wire.js";
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
4
+ import { randomUUID } from "node:crypto";
5
+ import { installModelSelection } from "@deepseek-ai/dsh-agent";
6
+ import { SessionId } from "@deepseek-ai/dsh-session";
7
+ import { StringDecoder } from "node:string_decoder";
8
+ //#region lib/types/agent-loader.js
9
+ /**
10
+ * Agent loading and turn aggregation shared by the one-shot runner and the
11
+ * serve loop: both drive a direct Agent over the core registry with the same
12
+ * model selection, the same scoped installModelSelection setup, the same
13
+ * preset join, and the same caller-supplied session identity, differing only
14
+ * in process lifetime.
15
+ * @module @khorsheed/dsh-local-agent-dsh-headless/agent-loader
16
+ */
17
+ /**
18
+ * Aggregate the last assistant text and turn outcome in one owned interval.
19
+ * @param events - the agent session's full event log.
20
+ * @param firstSeq - the session seq at which this driver's interval opened.
21
+ */
22
+ function summarizeTurn(events, firstSeq) {
23
+ let started = false;
24
+ let text = "";
25
+ let reason;
26
+ for (const event of events) {
27
+ if (event.seq < firstSeq) continue;
28
+ if (event.type === "turn/start") {
29
+ started = true;
30
+ continue;
31
+ }
32
+ if (!started) continue;
33
+ if (event.type === "assistant/message") {
34
+ const joined = event.data.message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
35
+ if (joined !== "") text = joined;
36
+ }
37
+ if (event.type === "turn/end") reason = event.data.reason;
38
+ }
39
+ return {
40
+ text,
41
+ reason
42
+ };
43
+ }
44
+ /**
45
+ * Apply a `--model` request onto the instance's default selection.
46
+ *
47
+ * The request is spelled `provider/model` — the same shape
48
+ * `effectiveSettings.model` reports the configured model in — and splits at
49
+ * the FIRST slash, so a provider-qualified id survives a model id that
50
+ * contains none. A request with no slash names the MODEL only and keeps the
51
+ * default selection's provider, which is what a caller switching between two
52
+ * models of the same route means.
53
+ *
54
+ * Everything else on the selection is carried through untouched (the
55
+ * reasoning effort in particular): `--model` changes the model, not the rest
56
+ * of the instance's configuration. An absent or blank request returns the
57
+ * default selection unchanged — byte for byte the behavior before the flag
58
+ * existed.
59
+ * @param selection - the instance's default model selection.
60
+ * @param request - the `--model` value, when the launch carried one.
61
+ * @returns the selection this launch runs.
62
+ */
63
+ function applyModelRequest(selection, request) {
64
+ const wanted = request?.trim();
65
+ if (wanted === void 0 || wanted === "") return selection;
66
+ const slash = wanted.indexOf("/");
67
+ if (slash <= 0 || slash === wanted.length - 1) return {
68
+ ...selection,
69
+ model: wanted
70
+ };
71
+ return {
72
+ ...selection,
73
+ provider: wanted.slice(0, slash),
74
+ model: wanted.slice(slash + 1)
75
+ };
76
+ }
77
+ /**
78
+ * Join this agent to the sub-profile's preset, when the sub-profile composes
79
+ * a roster.
80
+ *
81
+ * Without a roster this is a no-op and the agent reads the model-facing rows
82
+ * off the global layer — byte for byte the behavior of every sub-dsh before
83
+ * the roster layer existed, and the right answer for a rosterless
84
+ * composition (`@deepseek-ai/dsh-agent-presets` README, "Composing a child
85
+ * agent"). With one, the preset decides the tools, prompt sections and
86
+ * skills of every session this process hosts, which is what makes the
87
+ * scope's capability face a per-condition factor instead of an instance-wide
88
+ * constant.
89
+ *
90
+ * A roster that REFUSES (unknown preset id, broken composition) is not
91
+ * degraded past: mounting rejects, the agent creation rolls back, and the
92
+ * launch fails naming the preset. A sub-dsh that silently ran the global
93
+ * layer after being told to run preset X would attribute the round to a
94
+ * capability face it never had.
95
+ * @param ctx - plugin context (where the roster service is resolved).
96
+ * @param agentCtx - the agent's scope context, which joins the standing mount.
97
+ * @returns the preset id joined, or undefined when the composition has no roster.
98
+ */
99
+ async function joinSubDshPreset(ctx, agentCtx) {
100
+ const roster = ctx.get("agentPresets");
101
+ if (roster?.mount === void 0) return void 0;
102
+ return (await roster.mount(agentCtx)).id;
103
+ }
104
+ /**
105
+ * Create or resume the direct Agent for one caller-named session. The caller
106
+ * resolved the core services first (both call sites bail out on a disposed
107
+ * tree). The caller-supplied id is authoritative for both branches: the
108
+ * parent provider generated one uuid and passes the same value on every round
109
+ * of the same delegation, so resume finds exactly the sub-dsh session the
110
+ * fresh round created.
111
+ * @param ctx - plugin context carrying the Agent registry and default model.
112
+ * @param identity - fresh (`sessionId`) or continuation (`resumeSessionId`).
113
+ * @returns the owned agent handle (its disposer unwinds the agent's world).
114
+ */
115
+ async function loadSubDshAgent(ctx, identity) {
116
+ const agents = ctx.get("agents");
117
+ if (agents === void 0) throw new Error("local-agent-dsh-headless: the agents service is not mounted");
118
+ const selection = await resolveSubDshSelection(ctx, identity.model, identity.effort);
119
+ const agentOptions = {
120
+ provider: selection.provider,
121
+ model: selection.model
122
+ };
123
+ const setup = async (agentCtx) => {
124
+ installModelSelection(agentCtx, {
125
+ current: selection,
126
+ assembled: void 0
127
+ });
128
+ await joinSubDshPreset(ctx, agentCtx);
129
+ };
130
+ if (identity.resumeSessionId !== void 0) return agents.resume({
131
+ resumeSessionId: SessionId(identity.resumeSessionId),
132
+ agentOptions,
133
+ setup
134
+ });
135
+ return agents.create({
136
+ sessionId: SessionId(identity.sessionId ?? `session-${randomUUID()}`),
137
+ meta: { cwd: process.cwd() },
138
+ agentOptions,
139
+ setup
140
+ });
141
+ }
142
+ /** Validate the actual sub-instance's configuration without creating a session or generating. */
143
+ async function resolveSubDshSelection(ctx, model, effort, validateModel = false) {
144
+ const defaults = ctx.get("agentDefaultModel");
145
+ if (defaults === void 0) throw new Error("local-agent-dsh-headless: agentDefaultModel is not mounted");
146
+ const selection = applyModelRequest(defaults.currentSelection(), model);
147
+ if (effort === void 0 && !validateModel) return selection;
148
+ let timer;
149
+ try {
150
+ const info = await Promise.race([ctx.get("llm")?.resolveModelInfo(selection.provider, selection.model, AbortSignal.timeout(5e3)), new Promise((_resolve, reject) => {
151
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Sub-DSH model validation timed out")), 5e3);
152
+ timer.unref();
153
+ })]);
154
+ if (info === void 0) throw new Error("The sub-DSH adapter cannot resolve the requested model");
155
+ if (effort === void 0) return selection;
156
+ const native = info.reasoning?.efforts.find((option) => String(option.id) === effort);
157
+ if (native === void 0) throw new Error("The sub-DSH model does not advertise the requested reasoning effort");
158
+ return {
159
+ ...selection,
160
+ reasoningEffort: native.id
161
+ };
162
+ } finally {
163
+ if (timer !== void 0) clearTimeout(timer);
164
+ }
165
+ }
166
+ //#endregion
167
+ //#region lib/types/serve.js
168
+ /**
169
+ * Serve mode: the resident sub-dsh loop behind the family-internal live-driver
170
+ * wire (see `./wire.ts`). Where the one-shot runner drives a single task and
171
+ * exits, serve mode keeps the tree alive on stdin: each `turn/start` request
172
+ * feeds one followup into the caller-named session's Agent, every session
173
+ * event streams back as a `session/event` notification while it happens, and
174
+ * `session/idle` closes the turn after the log flush. `turn/interrupt`
175
+ * delivers the runtime-level graceful cancel (`Agent.cancel`) that the
176
+ * process boundary cannot express — the process survives and the session
177
+ * stays continuable, which is the live driver's reason to exist.
178
+ * @module @khorsheed/dsh-local-agent-dsh-headless/serve
179
+ */
180
+ /** Flatten an unknown thrown value for a wire error or stderr line. */
181
+ function messageOf(error) {
182
+ return error instanceof Error ? error.message : String(error);
183
+ }
184
+ /**
185
+ * Run the resident loop until stdin closes or a `shutdown` request arrives.
186
+ * @param ctx - plugin context carrying the loader settle, core services, and
187
+ * the session/event feed.
188
+ * @param io - process-facing effects.
189
+ */
190
+ async function runServe(ctx, io, model, effort) {
191
+ await ctx.get("loader")?.await();
192
+ const agents = ctx.get("agents");
193
+ const sessions = ctx.get("sessions");
194
+ if (agents === void 0 || sessions === void 0 || ctx.get("agentDefaultModel") === void 0) {
195
+ io.stderr.write("dsh: serve mode requires the agents, sessions, and agentDefaultModel services\n");
196
+ io.exit(1);
197
+ return;
198
+ }
199
+ /** Loaded agents by caller session id; one member per process in practice. */
200
+ const managed = /* @__PURE__ */ new Map();
201
+ /** Per-session turn serialization: one in-flight turn per session, FIFO. */
202
+ const queues = /* @__PURE__ */ new Map();
203
+ /**
204
+ * The in-flight round's parent-supplied turn number per session, set while
205
+ * a turn body runs and echoed on its notifications — the parent drops a
206
+ * cancelled round's late unwind instead of mis-settling the next round.
207
+ */
208
+ const activeTurns = /* @__PURE__ */ new Map();
209
+ let shuttingDown = false;
210
+ let buffer = "";
211
+ const decoder = new StringDecoder("utf8");
212
+ const send = (message) => {
213
+ io.stdout.write(JSON.stringify(message) + "\n");
214
+ };
215
+ const respond = (id, result) => {
216
+ send({
217
+ jsonrpc: "2.0",
218
+ id,
219
+ result
220
+ });
221
+ };
222
+ const respondError = (id, message, code = LIVE_WIRE_ERROR_INTERNAL) => {
223
+ send({
224
+ jsonrpc: "2.0",
225
+ id,
226
+ error: {
227
+ code,
228
+ message
229
+ }
230
+ });
231
+ };
232
+ const notify = (method, params) => {
233
+ send({
234
+ jsonrpc: "2.0",
235
+ method,
236
+ params
237
+ });
238
+ };
239
+ ctx.on("session/event", (session, event) => {
240
+ if (shuttingDown || !managed.has(String(session.id))) return;
241
+ const id = String(session.id);
242
+ notify("session/event", {
243
+ sessionId: id,
244
+ turn: activeTurns.get(id) ?? null,
245
+ event
246
+ });
247
+ });
248
+ ctx.on("agent/assistant-stream", ({ agent, frame }) => {
249
+ const sessionId = String(agent.session.id);
250
+ const turn = activeTurns.get(sessionId);
251
+ if (shuttingDown || !managed.has(sessionId) || turn === void 0) return;
252
+ notify("session/assistant-stream", {
253
+ sessionId,
254
+ turn,
255
+ frame
256
+ });
257
+ }, { global: true });
258
+ const debug = process.env["DSH_SERVE_DEBUG"] === "1" ? (message) => {
259
+ io.stderr.write(`dsh-serve: ${message}\n`);
260
+ } : () => {};
261
+ const shutdown = async () => {
262
+ if (shuttingDown) return;
263
+ shuttingDown = true;
264
+ debug("shutdown requested");
265
+ for (const handle of managed.values()) await handle.dispose().catch(() => {});
266
+ debug("managed agents disposed; requesting exit");
267
+ io.exit(0);
268
+ const stdin = io.stdin;
269
+ stdin.removeAllListeners?.("data");
270
+ stdin.removeAllListeners?.("end");
271
+ stdin.unref?.();
272
+ setTimeout(() => {
273
+ debug("exit fallback fired");
274
+ process.exit(0);
275
+ }, 2e3).unref();
276
+ };
277
+ /** Accept one turn: followup now, outcome later as `session/idle`. */
278
+ const startTurn = (sessionId, text, turn, agent) => {
279
+ const run = (queues.get(sessionId) ?? Promise.resolve()).then(async () => {
280
+ activeTurns.set(sessionId, turn);
281
+ let reason;
282
+ try {
283
+ const firstSeq = agent.session.seq;
284
+ agent.followup(createUserMessage({
285
+ content: [{
286
+ type: "text",
287
+ text
288
+ }],
289
+ source: { kind: "user" }
290
+ }));
291
+ await agent.whenIdle();
292
+ await sessions.flush(agent.session);
293
+ reason = summarizeTurn(agent.session.snapshotEvents(), firstSeq).reason ?? null;
294
+ } catch (error) {
295
+ reason = {
296
+ kind: "error",
297
+ error: {
298
+ message: messageOf(error),
299
+ code: "UNKNOWN"
300
+ }
301
+ };
302
+ }
303
+ const params = {
304
+ sessionId,
305
+ turn,
306
+ reason
307
+ };
308
+ try {
309
+ notify("session/idle", params);
310
+ } finally {
311
+ activeTurns.delete(sessionId);
312
+ }
313
+ });
314
+ queues.set(sessionId, run);
315
+ };
316
+ const handleRequest = async (id, method, params) => {
317
+ switch (method) {
318
+ case "initialize":
319
+ respond(id, {
320
+ serverInfo: { name: LIVE_SERVER_NAME },
321
+ protocolVersion: 1
322
+ });
323
+ return;
324
+ case "session/prepare":
325
+ try {
326
+ await resolveSubDshSelection(ctx, model, effort, true);
327
+ respond(id, { prepared: true });
328
+ } catch (error) {
329
+ respondError(id, messageOf(error));
330
+ }
331
+ return;
332
+ case "turn/start": {
333
+ const sessionId = params["sessionId"];
334
+ const text = params["text"];
335
+ const turn = params["turn"];
336
+ const resume = params["resume"] === true;
337
+ if (typeof sessionId !== "string" || sessionId === "" || typeof text !== "string" || typeof turn !== "number" || !Number.isSafeInteger(turn) || turn < 1) {
338
+ respondError(id, "turn/start requires a sessionId, a text, and a positive integer turn");
339
+ return;
340
+ }
341
+ let handle = managed.get(sessionId);
342
+ if (handle === void 0) {
343
+ try {
344
+ handle = await loadSubDshAgent(ctx, {
345
+ ...resume ? { resumeSessionId: sessionId } : { sessionId },
346
+ ...model === void 0 ? {} : { model },
347
+ ...effort === void 0 ? {} : { effort }
348
+ });
349
+ } catch (error) {
350
+ respondError(id, messageOf(error));
351
+ return;
352
+ }
353
+ managed.set(sessionId, handle);
354
+ }
355
+ startTurn(sessionId, text, turn, handle.agent);
356
+ respond(id, { accepted: true });
357
+ return;
358
+ }
359
+ case "turn/interrupt": {
360
+ const sessionId = params["sessionId"];
361
+ if (typeof sessionId !== "string") {
362
+ respondError(id, "turn/interrupt requires a sessionId");
363
+ return;
364
+ }
365
+ const handle = managed.get(sessionId);
366
+ handle?.agent.cancel({ kind: "parent" });
367
+ respond(id, { interrupted: handle !== void 0 });
368
+ return;
369
+ }
370
+ case "shutdown":
371
+ respond(id, {});
372
+ shutdown();
373
+ return;
374
+ default: respondError(id, `unknown method: ${method}`, LIVE_WIRE_ERROR_METHOD_NOT_FOUND);
375
+ }
376
+ };
377
+ io.stdin.on("data", (chunk) => {
378
+ if (shuttingDown) return;
379
+ const text = typeof chunk === "string" ? chunk : decoder.write(chunk);
380
+ buffer += text;
381
+ let index = buffer.indexOf("\n");
382
+ while (index >= 0) {
383
+ const line = buffer.slice(0, index);
384
+ buffer = buffer.slice(index + 1);
385
+ index = buffer.indexOf("\n");
386
+ if (line.trim() === "") continue;
387
+ let message;
388
+ try {
389
+ message = JSON.parse(line);
390
+ } catch {
391
+ io.stderr.write("dsh: serve ignored a malformed wire line\n");
392
+ continue;
393
+ }
394
+ if ((typeof message.id === "number" || typeof message.id === "string") && typeof message.method === "string") {
395
+ const id = Number(message.id);
396
+ handleRequest(id, message.method, message.params ?? {}).catch((error) => {
397
+ respondError(id, messageOf(error));
398
+ });
399
+ }
400
+ }
401
+ });
402
+ io.stdin.on("end", () => {
403
+ shutdown();
404
+ });
405
+ }
406
+ //#endregion
407
+ //#region lib/types/index.js
408
+ /**
409
+ * @khorsheed/dsh-local-agent-dsh-headless — sub-dsh direct Agent driver with
410
+ * two modes. One-shot (default): create one Agent through the core registry
411
+ * on a caller-supplied session id (or resume one), drive the task to
412
+ * quiescence, flush its Session, print the final assistant text, and exit.
413
+ * Serve (`--serve`): stay resident and drive turns over the family-internal
414
+ * live-driver wire (see `./serve.ts` / `./wire.ts`). The session id comes
415
+ * from the invocation (`--session-id` / `--resume`) or the wire, never from
416
+ * stdout — the parent provider generates one uuid and both sides use it, so
417
+ * stdout carries no parseable session marker.
418
+ *
419
+ * @module @khorsheed/dsh-local-agent-dsh-headless
420
+ */
421
+ /** Stable Cordis plugin name. */
422
+ const name = "local-agent-dsh-headless-runner";
423
+ /** Core services required before the one-shot turn can start. */
424
+ const inject = [
425
+ "agentDefaultModel",
426
+ "agents",
427
+ "sessions"
428
+ ];
429
+ const Config = z.object({
430
+ task: z.string(),
431
+ sessionId: z.string(),
432
+ resumeSessionId: z.string(),
433
+ serve: z.boolean().default(false),
434
+ model: z.string(),
435
+ effort: z.string()
436
+ });
437
+ /** The process streams the runner writes to; tests substitute captures. */
438
+ const internals = {
439
+ stdin: process.stdin,
440
+ stdout: process.stdout,
441
+ stderr: process.stderr
442
+ };
443
+ /** Report an unexpected direct-driver failure and request a failing exit. */
444
+ function fail(io, error) {
445
+ io.stderr.write(`dsh: ${error instanceof Error ? error.message : String(error)}\n`);
446
+ io.exit(1);
447
+ }
448
+ /**
449
+ * Run one task through a fresh or resumed Agent and request process exit.
450
+ * @param ctx - plugin context carrying the Agent, default model, Session, and launcher IO services.
451
+ * @param config - validated one-shot config (task plus the caller session id).
452
+ * @param io - process-facing effects.
453
+ */
454
+ async function run(ctx, config, io) {
455
+ await ctx.get("loader")?.await();
456
+ const sessions = ctx.get("sessions");
457
+ if (ctx.get("agents") === void 0 || ctx.get("agentDefaultModel") === void 0 || sessions === void 0) return;
458
+ const agent = (await loadSubDshAgent(ctx, {
459
+ ...config.sessionId === void 0 ? {} : { sessionId: config.sessionId },
460
+ ...config.resumeSessionId === void 0 ? {} : { resumeSessionId: config.resumeSessionId },
461
+ ...config.model === void 0 ? {} : { model: config.model },
462
+ ...config.effort === void 0 ? {} : { effort: config.effort }
463
+ })).agent;
464
+ await agent.whenIdle();
465
+ const firstSeq = agent.session.seq;
466
+ agent.followup(createUserMessage({
467
+ content: [{
468
+ type: "text",
469
+ text: config.task ?? ""
470
+ }],
471
+ source: { kind: "user" }
472
+ }));
473
+ await agent.whenIdle();
474
+ await sessions.flush(agent.session);
475
+ const outcome = summarizeTurn(agent.session.snapshotEvents(), firstSeq);
476
+ io.stdout.write(outcome.text + "\n");
477
+ if (outcome.reason?.kind === "error") io.stderr.write(`dsh: ${outcome.reason.error.code}: ${outcome.reason.error.message}\n`);
478
+ io.exit(outcome.reason?.kind === "completed" ? 0 : 1);
479
+ }
480
+ /**
481
+ * Mount the direct driver: one-shot by default, the resident serve loop when
482
+ * the invocation passed `--serve`.
483
+ * @param ctx - plugin context carrying core services and the launcher-provided exit request.
484
+ * @param config - validated task/serve config.
485
+ */
486
+ function apply(ctx, config) {
487
+ const exit = ctx.get("appExit");
488
+ if (exit === void 0) throw new Error("local-agent-dsh-headless-runner: the launcher must provide ctx.appExit before the tree mounts");
489
+ const io = {
490
+ stdin: internals.stdin,
491
+ stdout: internals.stdout,
492
+ stderr: internals.stderr,
493
+ exit
494
+ };
495
+ if (config.serve === true) {
496
+ runServe(ctx, io, config.model, config.effort).catch((error) => {
497
+ fail(io, error);
498
+ });
499
+ return;
500
+ }
501
+ if (config.task === void 0 || config.task.trim() === "") throw new Error("local-agent-dsh-headless-runner: a task is required unless serve mode is on");
502
+ run(ctx, config, io).catch((error) => {
503
+ fail(io, error);
504
+ });
505
+ }
506
+ //#endregion
507
+ export { Config, apply, inject, internals, name };
@@ -0,0 +1,36 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@khorsheed/dsh-local-agent-dsh-headless`.
4
+ * @module @khorsheed/dsh-local-agent-dsh-headless/invariant
5
+ */
6
+ const PACKAGE_NAME = "@khorsheed/dsh-local-agent-dsh-headless";
7
+ /** Cordis companion plugin name. */
8
+ const name = "local-agent-dsh-headless-invariant";
9
+ /** Service required before the companion can register. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * Composition guard: this bundle is sub-profile-only. Its patch rows
13
+ * (persona override, `hmr` off, `tools` mode, the `ptc-runtime` insert, the
14
+ * member-bridge MCP row) collide with or leak into any Host/web composition —
15
+ * 2026-08-23 P0: reconciled into the prod web profile as a direct dependency
16
+ * (reconcilePlugins auto-mounts every `dsh.bundle`-declaring direct dep), the
17
+ * `code-runtime` insert (today's `ptc-runtime` row) hit the web-app row's duplicate entry id and the
18
+ * whole instance failed to boot. The loader's duplicate-id failure is loud
19
+ * but cryptic, and only fires when the ids collide; this invariant is the
20
+ * CLEAR failure for the non-colliding case (e.g. an upstream row rename would
21
+ * otherwise leak the persona override into real user sessions). The web
22
+ * layer's `webStartup` service is the marker: in-box bundles mount before
23
+ * dependency-managed ones, so it is already provided when this companion
24
+ * applies.
25
+ */
26
+ const install = (ctx, fail) => {
27
+ if (ctx.get("webStartup") !== void 0) fail("mounted into a web composition — this bundle is sub-profile-only. Remove it from the profile’s direct dependencies: the parent local-agent-dsh bundle installs it transitively, and the sub-profile symlink resolves from that closure");
28
+ };
29
+ /**
30
+ * Register this package's invariant companion.
31
+ * @param ctx - Cordis context carrying the invariant service.
32
+ * @returns the installed registration's disposer after setup succeeds.
33
+ */
34
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
35
+ //#endregion
36
+ export { apply, inject, name };
package/lib/startup.js ADDED
@@ -0,0 +1,74 @@
1
+ import { Command } from "commander";
2
+ import { parseCmdline } from "@deepseek-ai/dsh-cmdline";
3
+ //#region lib/types/startup.js
4
+ /**
5
+ * The sub-dsh one-shot app's command-line provider: it parses the task
6
+ * positional, the caller-supplied `--session-id` (fresh delegation) and
7
+ * `--resume` (continuation) flags, and `--help`, then publishes
8
+ * {@link LOCAL_AGENT_DSH_HEADLESS_STARTUP_SERVICE}. The runner is an ordinary
9
+ * consumer whose lazy config waits for that service. The session id is
10
+ * caller-supplied — the parent provider generates one uuid and passes the same
11
+ * value to `agents.create`/`agents.resume` on the sub-dsh side, so stdout
12
+ * never carries a parseable session marker.
13
+ * @module @khorsheed/dsh-local-agent-dsh-headless/startup
14
+ */
15
+ /** Stable Cordis plugin name. */
16
+ const name = "local-agent-dsh-headless-startup";
17
+ /** Services required before the invocation can be resolved. */
18
+ const inject = ["cmdlineArgs"];
19
+ /** Service provided by this plugin and injected by the one-shot runner. */
20
+ const LOCAL_AGENT_DSH_HEADLESS_STARTUP_SERVICE = "localAgentDshHeadlessStartup";
21
+ /**
22
+ * This app's command: the session flags, the task positional, and help text.
23
+ * @returns a fresh program, so one process can parse more than once (tests).
24
+ */
25
+ function headlessCommand() {
26
+ return new Command().name("dsh --profile headless-local-agent-dsh").description("Answer one task in a caller-named session, print the final assistant message, and exit.").helpOption("-h, --help", "show this help").option("--session-id <id>", "create a fresh session with exactly this id").option("--resume <id>", "continue the existing session with this id").option("--serve", "stay resident and drive turns over the stdio wire (no task)").option("--model <provider/model>", "run this model instead of the instance default (a bare id keeps the provider)").option("--effort <value>", "native reasoning effort for this launch").argument("[task...]", "the task text; multiple words are joined by spaces").addHelpText("after", `
27
+ Examples:
28
+ dsh --profile headless-local-agent-dsh --session-id 6ba7... "run the tests" create one session and answer
29
+ dsh --profile headless-local-agent-dsh --resume 6ba7... "run the rest" continue the same session
30
+ dsh --profile headless-local-agent-dsh --serve resident live-driver mode (stdio wire)
31
+ dsh --profile headless-local-agent-dsh --model deepseek-official/deepseek-v4-pro --session-id 6ba7... "run the tests"
32
+ `);
33
+ }
34
+ /**
35
+ * Parse and provide the one-shot invocation as an ordinary Cordis service.
36
+ * The command's action publishes the invocation; a missing or whitespace-only
37
+ * task, or both session flags together, is a usage error, so on rejection (and
38
+ * on `--help`) nothing is provided.
39
+ * @param ctx - plugin context carrying the command line.
40
+ */
41
+ function apply(ctx) {
42
+ const program = headlessCommand();
43
+ program.action(() => {
44
+ const task = program.args.join(" ");
45
+ const options = program.opts();
46
+ const effort = options.effort?.trim();
47
+ if (effort === "") program.error("error: --effort needs a native reasoning value");
48
+ const model = options.model?.trim();
49
+ if (model !== void 0 && model === "") program.error("error: --model needs a model identifier, for example deepseek-official/deepseek-v4-pro");
50
+ if (options.serve === true) {
51
+ if (options.sessionId !== void 0 || options.resume !== void 0) program.error("error: --serve drives sessions over the wire; --session-id/--resume do not apply");
52
+ if (task.trim() !== "") program.error("error: --serve takes no task; turns arrive over the wire");
53
+ ctx.provide(LOCAL_AGENT_DSH_HEADLESS_STARTUP_SERVICE, {
54
+ task: "",
55
+ serve: true,
56
+ ...model === void 0 ? {} : { model },
57
+ ...effort === void 0 ? {} : { effort }
58
+ });
59
+ return;
60
+ }
61
+ if (task.trim() === "") program.error("error: a task is required, for example: dsh --profile headless-local-agent-dsh --session-id <id> \"run the tests\"");
62
+ if (options.sessionId !== void 0 && options.resume !== void 0) program.error("error: --session-id and --resume are mutually exclusive");
63
+ ctx.provide(LOCAL_AGENT_DSH_HEADLESS_STARTUP_SERVICE, {
64
+ task,
65
+ ...options.sessionId !== void 0 ? { sessionId: options.sessionId } : {},
66
+ ...options.resume !== void 0 ? { resumeSessionId: options.resume } : {},
67
+ ...model === void 0 ? {} : { model },
68
+ ...effort === void 0 ? {} : { effort }
69
+ });
70
+ });
71
+ parseCmdline(ctx, program);
72
+ }
73
+ //#endregion
74
+ export { LOCAL_AGENT_DSH_HEADLESS_STARTUP_SERVICE, apply, inject, name };