@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/README.en.md +97 -0
- package/README.i18n.yaml +6 -0
- package/README.md +97 -0
- package/cordis.patch.yml +81 -0
- package/lib/index.js +507 -0
- package/lib/invariant.js +36 -0
- package/lib/startup.js +74 -0
- package/lib/types/agent-loader.d.ts +85 -0
- package/lib/types/agent-loader.js +173 -0
- package/lib/types/index.d.ts +69 -0
- package/lib/types/index.js +99 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +41 -0
- package/lib/types/serve.d.ts +39 -0
- package/lib/types/serve.js +249 -0
- package/lib/types/startup.d.ts +47 -0
- package/lib/types/startup.js +96 -0
- package/lib/types/wire.d.ts +116 -0
- package/lib/types/wire.js +24 -0
- package/lib/wire.js +27 -0
- package/package.json +65 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serve mode: the resident sub-dsh loop behind the family-internal live-driver
|
|
3
|
+
* wire (see `./wire.ts`). Where the one-shot runner drives a single task and
|
|
4
|
+
* exits, serve mode keeps the tree alive on stdin: each `turn/start` request
|
|
5
|
+
* feeds one followup into the caller-named session's Agent, every session
|
|
6
|
+
* event streams back as a `session/event` notification while it happens, and
|
|
7
|
+
* `session/idle` closes the turn after the log flush. `turn/interrupt`
|
|
8
|
+
* delivers the runtime-level graceful cancel (`Agent.cancel`) that the
|
|
9
|
+
* process boundary cannot express — the process survives and the session
|
|
10
|
+
* stays continuable, which is the live driver's reason to exist.
|
|
11
|
+
* @module @khorsheed/dsh-local-agent-dsh-headless/serve
|
|
12
|
+
*/
|
|
13
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
14
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
15
|
+
import { resolveSubDshSelection, loadSubDshAgent, summarizeTurn } from './agent-loader.js';
|
|
16
|
+
import { LIVE_SERVER_NAME, LIVE_WIRE_ERROR_INTERNAL, LIVE_WIRE_ERROR_METHOD_NOT_FOUND, LIVE_WIRE_PROTOCOL_VERSION, } from './wire.js';
|
|
17
|
+
/** Flatten an unknown thrown value for a wire error or stderr line. */
|
|
18
|
+
function messageOf(error) {
|
|
19
|
+
return error instanceof Error ? error.message : String(error);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Run the resident loop until stdin closes or a `shutdown` request arrives.
|
|
23
|
+
* @param ctx - plugin context carrying the loader settle, core services, and
|
|
24
|
+
* the session/event feed.
|
|
25
|
+
* @param io - process-facing effects.
|
|
26
|
+
*/
|
|
27
|
+
export async function runServe(ctx, io, model, effort) {
|
|
28
|
+
// Loader siblings mount concurrently; await the complete application before
|
|
29
|
+
// driving Agents so scoped tools and adapters are not half-composed.
|
|
30
|
+
await ctx.get('loader')?.await();
|
|
31
|
+
const agents = ctx.get('agents');
|
|
32
|
+
const sessions = ctx.get('sessions');
|
|
33
|
+
if (agents === undefined || sessions === undefined || ctx.get('agentDefaultModel') === undefined) {
|
|
34
|
+
io.stderr.write('dsh: serve mode requires the agents, sessions, and agentDefaultModel services\n');
|
|
35
|
+
io.exit(1);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
/** Loaded agents by caller session id; one member per process in practice. */
|
|
39
|
+
const managed = new Map();
|
|
40
|
+
/** Per-session turn serialization: one in-flight turn per session, FIFO. */
|
|
41
|
+
const queues = new Map();
|
|
42
|
+
/**
|
|
43
|
+
* The in-flight round's parent-supplied turn number per session, set while
|
|
44
|
+
* a turn body runs and echoed on its notifications — the parent drops a
|
|
45
|
+
* cancelled round's late unwind instead of mis-settling the next round.
|
|
46
|
+
*/
|
|
47
|
+
const activeTurns = new Map();
|
|
48
|
+
let shuttingDown = false;
|
|
49
|
+
let buffer = '';
|
|
50
|
+
const decoder = new StringDecoder('utf8');
|
|
51
|
+
const send = (message) => {
|
|
52
|
+
io.stdout.write(JSON.stringify(message) + '\n');
|
|
53
|
+
};
|
|
54
|
+
const respond = (id, result) => {
|
|
55
|
+
send({ jsonrpc: '2.0', id, result });
|
|
56
|
+
};
|
|
57
|
+
const respondError = (id, message, code = LIVE_WIRE_ERROR_INTERNAL) => {
|
|
58
|
+
send({ jsonrpc: '2.0', id, error: { code, message } });
|
|
59
|
+
};
|
|
60
|
+
const notify = (method, params) => {
|
|
61
|
+
send({ jsonrpc: '2.0', method, params });
|
|
62
|
+
};
|
|
63
|
+
// Push every session event of a managed session the moment it lands; the
|
|
64
|
+
// parent mirrors event-by-event instead of polling the log file. Events
|
|
65
|
+
// outside a round (agent maintenance, load-time scaffolding) carry a null
|
|
66
|
+
// turn tag and the parent ignores them.
|
|
67
|
+
ctx.on('session/event', (session, event) => {
|
|
68
|
+
if (shuttingDown || !managed.has(String(session.id)))
|
|
69
|
+
return;
|
|
70
|
+
const id = String(session.id);
|
|
71
|
+
notify('session/event', {
|
|
72
|
+
sessionId: id,
|
|
73
|
+
turn: activeTurns.get(id) ?? null,
|
|
74
|
+
event: event,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
// This process owns real Agents, so the public scoped stream bus is valid
|
|
78
|
+
// here. The parent presents these frames through the plugin's own channel.
|
|
79
|
+
ctx.on('agent/assistant-stream', ({ agent, frame }) => {
|
|
80
|
+
const sessionId = String(agent.session.id);
|
|
81
|
+
const turn = activeTurns.get(sessionId);
|
|
82
|
+
if (shuttingDown || !managed.has(sessionId) || turn === undefined)
|
|
83
|
+
return;
|
|
84
|
+
notify('session/assistant-stream', { sessionId, turn, frame });
|
|
85
|
+
}, { global: true });
|
|
86
|
+
const debug = process.env['DSH_SERVE_DEBUG'] === '1'
|
|
87
|
+
? (message) => { io.stderr.write(`dsh-serve: ${message}\n`); }
|
|
88
|
+
: () => { };
|
|
89
|
+
const shutdown = async () => {
|
|
90
|
+
if (shuttingDown)
|
|
91
|
+
return;
|
|
92
|
+
shuttingDown = true;
|
|
93
|
+
debug('shutdown requested');
|
|
94
|
+
for (const handle of managed.values()) {
|
|
95
|
+
await handle.dispose().catch(() => { });
|
|
96
|
+
}
|
|
97
|
+
debug('managed agents disposed; requesting exit');
|
|
98
|
+
io.exit(0);
|
|
99
|
+
// The launcher's bounded shutdown completes with `process.exitCode`, which
|
|
100
|
+
// only takes effect once the event loop drains — our flowing stdin pipe
|
|
101
|
+
// would hold it open forever (a one-shot never listens on stdin, so only
|
|
102
|
+
// serve mode hits this). Detach our stdin handle and arm a bounded
|
|
103
|
+
// self-exit fallback for anything else still holding the loop.
|
|
104
|
+
const stdin = io.stdin;
|
|
105
|
+
stdin.removeAllListeners?.('data');
|
|
106
|
+
stdin.removeAllListeners?.('end');
|
|
107
|
+
stdin.unref?.();
|
|
108
|
+
setTimeout(() => {
|
|
109
|
+
debug('exit fallback fired');
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}, 2_000).unref();
|
|
112
|
+
};
|
|
113
|
+
/** Accept one turn: followup now, outcome later as `session/idle`. */
|
|
114
|
+
const startTurn = (sessionId, text, turn, agent) => {
|
|
115
|
+
const previous = queues.get(sessionId) ?? Promise.resolve();
|
|
116
|
+
const run = previous.then(async () => {
|
|
117
|
+
activeTurns.set(sessionId, turn);
|
|
118
|
+
let reason;
|
|
119
|
+
try {
|
|
120
|
+
const firstSeq = agent.session.seq;
|
|
121
|
+
agent.followup(createUserMessage({
|
|
122
|
+
content: [{ type: 'text', text }],
|
|
123
|
+
source: { kind: 'user' },
|
|
124
|
+
}));
|
|
125
|
+
await agent.whenIdle();
|
|
126
|
+
// Flush BEFORE the idle notification so the parent's file-based
|
|
127
|
+
// reconciliation pass sees the complete round on disk.
|
|
128
|
+
await sessions.flush(agent.session);
|
|
129
|
+
reason = summarizeTurn(agent.session.snapshotEvents(), firstSeq).reason ?? null;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
// A turn-level failure must still close the parent's wait.
|
|
133
|
+
reason = { kind: 'error', error: { message: messageOf(error), code: 'UNKNOWN' } };
|
|
134
|
+
}
|
|
135
|
+
const params = { sessionId, turn, reason };
|
|
136
|
+
try {
|
|
137
|
+
notify('session/idle', params);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
activeTurns.delete(sessionId);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
queues.set(sessionId, run);
|
|
144
|
+
};
|
|
145
|
+
const handleRequest = async (id, method, params) => {
|
|
146
|
+
switch (method) {
|
|
147
|
+
case 'initialize':
|
|
148
|
+
respond(id, { serverInfo: { name: LIVE_SERVER_NAME }, protocolVersion: LIVE_WIRE_PROTOCOL_VERSION });
|
|
149
|
+
return;
|
|
150
|
+
case 'session/prepare': {
|
|
151
|
+
try {
|
|
152
|
+
await resolveSubDshSelection(ctx, model, effort, true);
|
|
153
|
+
respond(id, { prepared: true });
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
respondError(id, messageOf(error));
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
case 'turn/start': {
|
|
161
|
+
const sessionId = params['sessionId'];
|
|
162
|
+
const text = params['text'];
|
|
163
|
+
const turn = params['turn'];
|
|
164
|
+
const resume = params['resume'] === true;
|
|
165
|
+
if (typeof sessionId !== 'string' || sessionId === '' || typeof text !== 'string'
|
|
166
|
+
|| typeof turn !== 'number' || !Number.isSafeInteger(turn) || turn < 1) {
|
|
167
|
+
respondError(id, 'turn/start requires a sessionId, a text, and a positive integer turn');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
let handle = managed.get(sessionId);
|
|
171
|
+
if (handle === undefined) {
|
|
172
|
+
try {
|
|
173
|
+
// The launch's `--model` binds every session this resident
|
|
174
|
+
// process hosts: a runtime's model is a process fact, which is
|
|
175
|
+
// exactly why the parent refuses a per-delegation model on the
|
|
176
|
+
// live path.
|
|
177
|
+
handle = await loadSubDshAgent(ctx, {
|
|
178
|
+
...resume ? { resumeSessionId: sessionId } : { sessionId },
|
|
179
|
+
...model === undefined ? {} : { model },
|
|
180
|
+
...effort === undefined ? {} : { effort },
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
respondError(id, messageOf(error));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
managed.set(sessionId, handle);
|
|
188
|
+
}
|
|
189
|
+
startTurn(sessionId, text, turn, handle.agent);
|
|
190
|
+
respond(id, { accepted: true });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
case 'turn/interrupt': {
|
|
194
|
+
const sessionId = params['sessionId'];
|
|
195
|
+
if (typeof sessionId !== 'string') {
|
|
196
|
+
respondError(id, 'turn/interrupt requires a sessionId');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const handle = managed.get(sessionId);
|
|
200
|
+
// The official Agent cancel: the active turn aborts, un-started inbox
|
|
201
|
+
// work survives, and the process/session stay continuable.
|
|
202
|
+
handle?.agent.cancel({ kind: 'parent' });
|
|
203
|
+
respond(id, { interrupted: handle !== undefined });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
case 'shutdown':
|
|
207
|
+
respond(id, {});
|
|
208
|
+
void shutdown();
|
|
209
|
+
return;
|
|
210
|
+
default:
|
|
211
|
+
respondError(id, `unknown method: ${method}`, LIVE_WIRE_ERROR_METHOD_NOT_FOUND);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
io.stdin.on('data', (chunk) => {
|
|
215
|
+
if (shuttingDown)
|
|
216
|
+
return;
|
|
217
|
+
// A multi-byte UTF-8 sequence can straddle two chunks; the StringDecoder
|
|
218
|
+
// holds the partial tail instead of corrupting it into U+FFFD.
|
|
219
|
+
const text = typeof chunk === 'string'
|
|
220
|
+
? chunk
|
|
221
|
+
: decoder.write(chunk);
|
|
222
|
+
buffer += text;
|
|
223
|
+
let index = buffer.indexOf('\n');
|
|
224
|
+
while (index >= 0) {
|
|
225
|
+
const line = buffer.slice(0, index);
|
|
226
|
+
buffer = buffer.slice(index + 1);
|
|
227
|
+
index = buffer.indexOf('\n');
|
|
228
|
+
if (line.trim() === '')
|
|
229
|
+
continue;
|
|
230
|
+
let message;
|
|
231
|
+
try {
|
|
232
|
+
message = JSON.parse(line);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
io.stderr.write('dsh: serve ignored a malformed wire line\n');
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
// Only requests carry an id; the parent sends no notifications.
|
|
239
|
+
if ((typeof message.id === 'number' || typeof message.id === 'string') && typeof message.method === 'string') {
|
|
240
|
+
const id = Number(message.id);
|
|
241
|
+
void handleRequest(id, message.method, (message.params ?? {})).catch((error) => {
|
|
242
|
+
respondError(id, messageOf(error));
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
// The parent closing the pipe IS the shutdown request (reclaim path).
|
|
248
|
+
io.stdin.on('end', () => { void shutdown(); });
|
|
249
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sub-dsh one-shot app's command-line provider: it parses the task
|
|
3
|
+
* positional, the caller-supplied `--session-id` (fresh delegation) and
|
|
4
|
+
* `--resume` (continuation) flags, and `--help`, then publishes
|
|
5
|
+
* {@link LOCAL_AGENT_DSH_HEADLESS_STARTUP_SERVICE}. The runner is an ordinary
|
|
6
|
+
* consumer whose lazy config waits for that service. The session id is
|
|
7
|
+
* caller-supplied — the parent provider generates one uuid and passes the same
|
|
8
|
+
* value to `agents.create`/`agents.resume` on the sub-dsh side, so stdout
|
|
9
|
+
* never carries a parseable session marker.
|
|
10
|
+
* @module @khorsheed/dsh-local-agent-dsh-headless/startup
|
|
11
|
+
*/
|
|
12
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
13
|
+
/** Stable Cordis plugin name. */
|
|
14
|
+
export declare const name = "local-agent-dsh-headless-startup";
|
|
15
|
+
/** Services required before the invocation can be resolved. */
|
|
16
|
+
export declare const inject: string[];
|
|
17
|
+
/** Service provided by this plugin and injected by the one-shot runner. */
|
|
18
|
+
export declare const LOCAL_AGENT_DSH_HEADLESS_STARTUP_SERVICE = "localAgentDshHeadlessStartup";
|
|
19
|
+
/** What the runner row reads from {@link LOCAL_AGENT_DSH_HEADLESS_STARTUP_SERVICE}. */
|
|
20
|
+
export interface LocalAgentDshHeadlessStartupValues {
|
|
21
|
+
/** The task text this invocation asked for; empty in serve mode. */
|
|
22
|
+
task: string;
|
|
23
|
+
/** Fresh delegation: create a session with this exact id. */
|
|
24
|
+
sessionId?: string;
|
|
25
|
+
/** Continuation: resume the existing session with this id. */
|
|
26
|
+
resumeSessionId?: string;
|
|
27
|
+
/** Resident mode: drive turns over the stdio wire, never exiting on idle. */
|
|
28
|
+
serve?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The model this launch runs, spelled `provider/model` (a bare id names the
|
|
31
|
+
* model and keeps the instance's provider). Absent means the instance's own
|
|
32
|
+
* default model selection, unchanged. Under `--serve` it applies to every
|
|
33
|
+
* session the resident process hosts — a resident runtime binds its model
|
|
34
|
+
* at spawn, which is why the parent refuses a per-delegation model there.
|
|
35
|
+
*/
|
|
36
|
+
model?: string;
|
|
37
|
+
effort?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parse and provide the one-shot invocation as an ordinary Cordis service.
|
|
41
|
+
* The command's action publishes the invocation; a missing or whitespace-only
|
|
42
|
+
* task, or both session flags together, is a usage error, so on rejection (and
|
|
43
|
+
* on `--help`) nothing is provided.
|
|
44
|
+
* @param ctx - plugin context carrying the command line.
|
|
45
|
+
*/
|
|
46
|
+
export declare function apply(ctx: Context): void;
|
|
47
|
+
//# sourceMappingURL=startup.d.ts.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sub-dsh one-shot app's command-line provider: it parses the task
|
|
3
|
+
* positional, the caller-supplied `--session-id` (fresh delegation) and
|
|
4
|
+
* `--resume` (continuation) flags, and `--help`, then publishes
|
|
5
|
+
* {@link LOCAL_AGENT_DSH_HEADLESS_STARTUP_SERVICE}. The runner is an ordinary
|
|
6
|
+
* consumer whose lazy config waits for that service. The session id is
|
|
7
|
+
* caller-supplied — the parent provider generates one uuid and passes the same
|
|
8
|
+
* value to `agents.create`/`agents.resume` on the sub-dsh side, so stdout
|
|
9
|
+
* never carries a parseable session marker.
|
|
10
|
+
* @module @khorsheed/dsh-local-agent-dsh-headless/startup
|
|
11
|
+
*/
|
|
12
|
+
import { Command } from 'commander';
|
|
13
|
+
import { parseCmdline } from '@deepseek-ai/dsh-cmdline';
|
|
14
|
+
/** Stable Cordis plugin name. */
|
|
15
|
+
export const name = 'local-agent-dsh-headless-startup';
|
|
16
|
+
/** Services required before the invocation can be resolved. */
|
|
17
|
+
export const inject = ['cmdlineArgs'];
|
|
18
|
+
/** Service provided by this plugin and injected by the one-shot runner. */
|
|
19
|
+
export const LOCAL_AGENT_DSH_HEADLESS_STARTUP_SERVICE = 'localAgentDshHeadlessStartup';
|
|
20
|
+
/**
|
|
21
|
+
* This app's command: the session flags, the task positional, and help text.
|
|
22
|
+
* @returns a fresh program, so one process can parse more than once (tests).
|
|
23
|
+
*/
|
|
24
|
+
function headlessCommand() {
|
|
25
|
+
return new Command()
|
|
26
|
+
.name('dsh --profile headless-local-agent-dsh')
|
|
27
|
+
.description('Answer one task in a caller-named session, print the final assistant message, and exit.')
|
|
28
|
+
.helpOption('-h, --help', 'show this help')
|
|
29
|
+
.option('--session-id <id>', 'create a fresh session with exactly this id')
|
|
30
|
+
.option('--resume <id>', 'continue the existing session with this id')
|
|
31
|
+
.option('--serve', 'stay resident and drive turns over the stdio wire (no task)')
|
|
32
|
+
.option('--model <provider/model>', 'run this model instead of the instance default (a bare id keeps the provider)')
|
|
33
|
+
.option('--effort <value>', 'native reasoning effort for this launch')
|
|
34
|
+
.argument('[task...]', 'the task text; multiple words are joined by spaces')
|
|
35
|
+
.addHelpText('after', `
|
|
36
|
+
Examples:
|
|
37
|
+
dsh --profile headless-local-agent-dsh --session-id 6ba7... "run the tests" create one session and answer
|
|
38
|
+
dsh --profile headless-local-agent-dsh --resume 6ba7... "run the rest" continue the same session
|
|
39
|
+
dsh --profile headless-local-agent-dsh --serve resident live-driver mode (stdio wire)
|
|
40
|
+
dsh --profile headless-local-agent-dsh --model deepseek-official/deepseek-v4-pro --session-id 6ba7... "run the tests"
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parse and provide the one-shot invocation as an ordinary Cordis service.
|
|
45
|
+
* The command's action publishes the invocation; a missing or whitespace-only
|
|
46
|
+
* task, or both session flags together, is a usage error, so on rejection (and
|
|
47
|
+
* on `--help`) nothing is provided.
|
|
48
|
+
* @param ctx - plugin context carrying the command line.
|
|
49
|
+
*/
|
|
50
|
+
export function apply(ctx) {
|
|
51
|
+
const program = headlessCommand();
|
|
52
|
+
program.action(() => {
|
|
53
|
+
const task = program.args.join(' ');
|
|
54
|
+
// Commander camelizes `--session-id` to `sessionId` but keeps `--resume`
|
|
55
|
+
// as `resume`; map both onto the service's explicit field names.
|
|
56
|
+
const options = program.opts();
|
|
57
|
+
// `--model` is orthogonal to the session flags: it names WHICH model runs,
|
|
58
|
+
// not which session, so it rides both modes.
|
|
59
|
+
const effort = options.effort?.trim();
|
|
60
|
+
if (effort === '')
|
|
61
|
+
program.error('error: --effort needs a native reasoning value');
|
|
62
|
+
const model = options.model?.trim();
|
|
63
|
+
if (model !== undefined && model === '') {
|
|
64
|
+
program.error('error: --model needs a model identifier, for example deepseek-official/deepseek-v4-pro');
|
|
65
|
+
}
|
|
66
|
+
if (options.serve === true) {
|
|
67
|
+
if (options.sessionId !== undefined || options.resume !== undefined) {
|
|
68
|
+
program.error('error: --serve drives sessions over the wire; --session-id/--resume do not apply');
|
|
69
|
+
}
|
|
70
|
+
if (task.trim() !== '') {
|
|
71
|
+
program.error('error: --serve takes no task; turns arrive over the wire');
|
|
72
|
+
}
|
|
73
|
+
ctx.provide(LOCAL_AGENT_DSH_HEADLESS_STARTUP_SERVICE, {
|
|
74
|
+
task: '',
|
|
75
|
+
serve: true,
|
|
76
|
+
...(model === undefined ? {} : { model }),
|
|
77
|
+
...(effort === undefined ? {} : { effort }),
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (task.trim() === '') {
|
|
82
|
+
program.error('error: a task is required, for example: dsh --profile headless-local-agent-dsh --session-id <id> "run the tests"');
|
|
83
|
+
}
|
|
84
|
+
if (options.sessionId !== undefined && options.resume !== undefined) {
|
|
85
|
+
program.error('error: --session-id and --resume are mutually exclusive');
|
|
86
|
+
}
|
|
87
|
+
ctx.provide(LOCAL_AGENT_DSH_HEADLESS_STARTUP_SERVICE, {
|
|
88
|
+
task,
|
|
89
|
+
...(options.sessionId !== undefined ? { sessionId: options.sessionId } : {}),
|
|
90
|
+
...(options.resume !== undefined ? { resumeSessionId: options.resume } : {}),
|
|
91
|
+
...(model === undefined ? {} : { model }),
|
|
92
|
+
...(effort === undefined ? {} : { effort }),
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
parseCmdline(ctx, program);
|
|
96
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The family-internal live-driver wire between the `local-agent-dsh` provider
|
|
3
|
+
* (parent side) and this bundle's serve mode (the resident sub-dsh). Newline-
|
|
4
|
+
* delimited JSON-RPC 2.0 over the sub-dsh's stdio, framed exactly like the
|
|
5
|
+
* official `JsonRpcLineTransport` (`@deepseek-ai/dsh-sdk-protocol`) so a
|
|
6
|
+
* future swap to the official SDK server stays mechanical. The official
|
|
7
|
+
* server is NOT usable today: its wire has no turn-level interrupt
|
|
8
|
+
* (`initialize` / `session/prompt` / `shutdown` only), and graceful interrupt
|
|
9
|
+
* is the live driver's core win — serve mode drives the in-process
|
|
10
|
+
* `Agent.cancel` instead (upstream seam registry, SDK-wire-interrupt entry).
|
|
11
|
+
*
|
|
12
|
+
* One resident process serves one member (one session). All requests are
|
|
13
|
+
* quick acknowledgements; the turn's events and terminal outcome flow back as
|
|
14
|
+
* notifications, never as the request response, so a long turn never holds a
|
|
15
|
+
* request open.
|
|
16
|
+
* @module @khorsheed/dsh-local-agent-dsh-headless/wire
|
|
17
|
+
*/
|
|
18
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
19
|
+
import type { AssistantStreamFrame } from '@deepseek-ai/dsh-agent';
|
|
20
|
+
/** Additive transient notification; old clients safely ignore unknown methods. */
|
|
21
|
+
export interface LiveAssistantStreamParams {
|
|
22
|
+
readonly sessionId: string;
|
|
23
|
+
readonly turn: number;
|
|
24
|
+
readonly frame: AssistantStreamFrame;
|
|
25
|
+
}
|
|
26
|
+
/** Wire contract version; bumped on any incompatible shape change. */
|
|
27
|
+
export declare const LIVE_WIRE_PROTOCOL_VERSION = 1;
|
|
28
|
+
/** The serve mode's identity, reported by the `initialize` handshake. */
|
|
29
|
+
export declare const LIVE_SERVER_NAME = "@khorsheed/dsh-local-agent-dsh-headless/serve";
|
|
30
|
+
/** A wire request (parent → sub-dsh). */
|
|
31
|
+
export interface LiveWireRequest {
|
|
32
|
+
readonly jsonrpc: '2.0';
|
|
33
|
+
readonly id: number;
|
|
34
|
+
readonly method: string;
|
|
35
|
+
readonly params?: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
/** A wire response (sub-dsh → parent): exactly one of result/error. */
|
|
38
|
+
export interface LiveWireResponse {
|
|
39
|
+
readonly jsonrpc: '2.0';
|
|
40
|
+
readonly id: number;
|
|
41
|
+
readonly result?: unknown;
|
|
42
|
+
readonly error?: {
|
|
43
|
+
readonly code: number;
|
|
44
|
+
readonly message: string;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** A wire notification (sub-dsh → parent). */
|
|
48
|
+
export interface LiveWireNotification {
|
|
49
|
+
readonly jsonrpc: '2.0';
|
|
50
|
+
readonly method: string;
|
|
51
|
+
readonly params?: Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
/** `initialize` result: the handshake proving the channel speaks this wire. */
|
|
54
|
+
export interface LiveInitializeResult {
|
|
55
|
+
readonly serverInfo: {
|
|
56
|
+
readonly name: string;
|
|
57
|
+
};
|
|
58
|
+
readonly protocolVersion: number;
|
|
59
|
+
}
|
|
60
|
+
/** `turn/start` params: drive one turn of the named session. */
|
|
61
|
+
export interface LiveTurnStartParams {
|
|
62
|
+
/** Caller-supplied session id (the parent-side child session id). */
|
|
63
|
+
readonly sessionId: string;
|
|
64
|
+
/** The turn's prompt text (the family's text-only task contract). */
|
|
65
|
+
readonly text: string;
|
|
66
|
+
/** True on a resume round: load the existing session, never create it. */
|
|
67
|
+
readonly resume: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* The parent's delegation-round number (fresh = 1, each resume +1). The
|
|
70
|
+
* serve side echoes it on every `session/event` / `session/idle` of this
|
|
71
|
+
* turn, so the parent can drop a cancelled round's late unwind
|
|
72
|
+
* notifications instead of letting them mis-settle the next round.
|
|
73
|
+
*/
|
|
74
|
+
readonly turn: number;
|
|
75
|
+
}
|
|
76
|
+
/** `turn/start` result: the turn was accepted; its outcome arrives as `session/idle`. */
|
|
77
|
+
export interface LiveTurnStartResult {
|
|
78
|
+
readonly accepted: true;
|
|
79
|
+
}
|
|
80
|
+
/** `turn/interrupt` params: gracefully cancel the named session's active turn. */
|
|
81
|
+
export interface LiveTurnInterruptParams {
|
|
82
|
+
readonly sessionId: string;
|
|
83
|
+
}
|
|
84
|
+
/** `turn/interrupt` result: whether the session is loaded in this runtime. */
|
|
85
|
+
export interface LiveTurnInterruptResult {
|
|
86
|
+
readonly interrupted: boolean;
|
|
87
|
+
}
|
|
88
|
+
/** The sub-dsh turn's terminal reason, mirrored from its `turn/end` event. */
|
|
89
|
+
export type LiveTurnReason = SessionEvent<'turn/end'>['data']['reason'];
|
|
90
|
+
/** `session/event` notification params: one live session event, in order. */
|
|
91
|
+
export interface LiveSessionEventParams {
|
|
92
|
+
readonly sessionId: string;
|
|
93
|
+
/**
|
|
94
|
+
* The round this event belongs to (the echo of `turn/start`'s `turn`), or
|
|
95
|
+
* null when the event landed outside any round — the parent mirrors only
|
|
96
|
+
* its own round's events.
|
|
97
|
+
*/
|
|
98
|
+
readonly turn: number | null;
|
|
99
|
+
readonly event: SessionEvent;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* `session/idle` notification params: the session's active turn settled and
|
|
103
|
+
* its session log was flushed to disk, so a file-based reconciliation pass
|
|
104
|
+
* right after this notification sees the complete round.
|
|
105
|
+
*/
|
|
106
|
+
export interface LiveSessionIdleParams {
|
|
107
|
+
readonly sessionId: string;
|
|
108
|
+
/** The round this idle closes (the echo of `turn/start`'s `turn`). */
|
|
109
|
+
readonly turn: number;
|
|
110
|
+
/** Null when the owned interval recorded no turn (a broken turn). */
|
|
111
|
+
readonly reason: LiveTurnReason | null;
|
|
112
|
+
}
|
|
113
|
+
/** JSON-RPC error codes this wire uses. */
|
|
114
|
+
export declare const LIVE_WIRE_ERROR_METHOD_NOT_FOUND = -32601;
|
|
115
|
+
export declare const LIVE_WIRE_ERROR_INTERNAL = -32603;
|
|
116
|
+
//# sourceMappingURL=wire.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The family-internal live-driver wire between the `local-agent-dsh` provider
|
|
3
|
+
* (parent side) and this bundle's serve mode (the resident sub-dsh). Newline-
|
|
4
|
+
* delimited JSON-RPC 2.0 over the sub-dsh's stdio, framed exactly like the
|
|
5
|
+
* official `JsonRpcLineTransport` (`@deepseek-ai/dsh-sdk-protocol`) so a
|
|
6
|
+
* future swap to the official SDK server stays mechanical. The official
|
|
7
|
+
* server is NOT usable today: its wire has no turn-level interrupt
|
|
8
|
+
* (`initialize` / `session/prompt` / `shutdown` only), and graceful interrupt
|
|
9
|
+
* is the live driver's core win — serve mode drives the in-process
|
|
10
|
+
* `Agent.cancel` instead (upstream seam registry, SDK-wire-interrupt entry).
|
|
11
|
+
*
|
|
12
|
+
* One resident process serves one member (one session). All requests are
|
|
13
|
+
* quick acknowledgements; the turn's events and terminal outcome flow back as
|
|
14
|
+
* notifications, never as the request response, so a long turn never holds a
|
|
15
|
+
* request open.
|
|
16
|
+
* @module @khorsheed/dsh-local-agent-dsh-headless/wire
|
|
17
|
+
*/
|
|
18
|
+
/** Wire contract version; bumped on any incompatible shape change. */
|
|
19
|
+
export const LIVE_WIRE_PROTOCOL_VERSION = 1;
|
|
20
|
+
/** The serve mode's identity, reported by the `initialize` handshake. */
|
|
21
|
+
export const LIVE_SERVER_NAME = '@khorsheed/dsh-local-agent-dsh-headless/serve';
|
|
22
|
+
/** JSON-RPC error codes this wire uses. */
|
|
23
|
+
export const LIVE_WIRE_ERROR_METHOD_NOT_FOUND = -32601;
|
|
24
|
+
export const LIVE_WIRE_ERROR_INTERNAL = -32603;
|
package/lib/wire.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region lib/types/wire.js
|
|
2
|
+
/**
|
|
3
|
+
* The family-internal live-driver wire between the `local-agent-dsh` provider
|
|
4
|
+
* (parent side) and this bundle's serve mode (the resident sub-dsh). Newline-
|
|
5
|
+
* delimited JSON-RPC 2.0 over the sub-dsh's stdio, framed exactly like the
|
|
6
|
+
* official `JsonRpcLineTransport` (`@deepseek-ai/dsh-sdk-protocol`) so a
|
|
7
|
+
* future swap to the official SDK server stays mechanical. The official
|
|
8
|
+
* server is NOT usable today: its wire has no turn-level interrupt
|
|
9
|
+
* (`initialize` / `session/prompt` / `shutdown` only), and graceful interrupt
|
|
10
|
+
* is the live driver's core win — serve mode drives the in-process
|
|
11
|
+
* `Agent.cancel` instead (upstream seam registry, SDK-wire-interrupt entry).
|
|
12
|
+
*
|
|
13
|
+
* One resident process serves one member (one session). All requests are
|
|
14
|
+
* quick acknowledgements; the turn's events and terminal outcome flow back as
|
|
15
|
+
* notifications, never as the request response, so a long turn never holds a
|
|
16
|
+
* request open.
|
|
17
|
+
* @module @khorsheed/dsh-local-agent-dsh-headless/wire
|
|
18
|
+
*/
|
|
19
|
+
/** Wire contract version; bumped on any incompatible shape change. */
|
|
20
|
+
const LIVE_WIRE_PROTOCOL_VERSION = 1;
|
|
21
|
+
/** The serve mode's identity, reported by the `initialize` handshake. */
|
|
22
|
+
const LIVE_SERVER_NAME = "@khorsheed/dsh-local-agent-dsh-headless/serve";
|
|
23
|
+
/** JSON-RPC error codes this wire uses. */
|
|
24
|
+
const LIVE_WIRE_ERROR_METHOD_NOT_FOUND = -32601;
|
|
25
|
+
const LIVE_WIRE_ERROR_INTERNAL = -32603;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { LIVE_SERVER_NAME, LIVE_WIRE_ERROR_INTERNAL, LIVE_WIRE_ERROR_METHOD_NOT_FOUND, LIVE_WIRE_PROTOCOL_VERSION };
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@khorsheed/dsh-local-agent-dsh-headless",
|
|
3
|
+
"description": "Sub-dsh headless bundle for the local-agent-dsh harness: a direct Agent/Session runner over dsh-base — one-shot (caller-supplied --session-id/--resume, prints the final assistant text, exits) or resident (--serve) driving turns over the family stdio wire",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"dsh",
|
|
6
|
+
"dsh-plugin",
|
|
7
|
+
"cordis",
|
|
8
|
+
"local-agent",
|
|
9
|
+
"headless"
|
|
10
|
+
],
|
|
11
|
+
"version": "0.1.0-rc.6",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "lib/index.js",
|
|
14
|
+
"types": "lib/types/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./lib/types/index.d.ts",
|
|
18
|
+
"default": "./lib/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./startup": {
|
|
21
|
+
"types": "./lib/types/startup.d.ts",
|
|
22
|
+
"default": "./lib/startup.js"
|
|
23
|
+
},
|
|
24
|
+
"./wire": {
|
|
25
|
+
"types": "./lib/types/wire.d.ts",
|
|
26
|
+
"default": "./lib/wire.js"
|
|
27
|
+
},
|
|
28
|
+
"./invariant": {
|
|
29
|
+
"types": "./lib/types/invariant.d.ts",
|
|
30
|
+
"default": "./lib/invariant.js"
|
|
31
|
+
},
|
|
32
|
+
"./src/*": "./src/*",
|
|
33
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"dsh": {
|
|
37
|
+
"composition": {
|
|
38
|
+
"component": "sub-profile-patch"
|
|
39
|
+
},
|
|
40
|
+
"compat": {
|
|
41
|
+
"minHost": "0.1.5-rc.1",
|
|
42
|
+
"verifiedHost": "0.1.5-rc.1"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"lib",
|
|
47
|
+
"cordis.patch.yml"
|
|
48
|
+
],
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
52
|
+
"@deepseek-ai/cordis-plugin-loader": "^1.0.2",
|
|
53
|
+
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
|
|
54
|
+
"@deepseek-ai/dsh-agent-default-model": "^0.1.0-rc.6",
|
|
55
|
+
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
|
|
56
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
57
|
+
"@deepseek-ai/dsh-mcp-client": "^0.1.0-rc.8",
|
|
58
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.6"
|
|
59
|
+
},
|
|
60
|
+
"peerDependenciesMeta": {
|
|
61
|
+
"@deepseek-ai/dsh-mcp-client": {
|
|
62
|
+
"optional": true
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|