@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,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent loading and turn aggregation shared by the one-shot runner and the
|
|
3
|
+
* serve loop: both drive a direct Agent over the core registry with the same
|
|
4
|
+
* model selection, the same scoped installModelSelection setup, the same
|
|
5
|
+
* preset join, and the same caller-supplied session identity, differing only
|
|
6
|
+
* in process lifetime.
|
|
7
|
+
* @module @khorsheed/dsh-local-agent-dsh-headless/agent-loader
|
|
8
|
+
*/
|
|
9
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
10
|
+
import type { AgentHandle, ModelSelection } from '@deepseek-ai/dsh-agent';
|
|
11
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
12
|
+
/** Outcome of one owned turn interval. */
|
|
13
|
+
export interface TurnOutcome {
|
|
14
|
+
text: string;
|
|
15
|
+
reason: SessionEvent<'turn/end'>['data']['reason'] | undefined;
|
|
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
|
+
export declare function summarizeTurn(events: readonly SessionEvent[], firstSeq: number): TurnOutcome;
|
|
23
|
+
/**
|
|
24
|
+
* Apply a `--model` request onto the instance's default selection.
|
|
25
|
+
*
|
|
26
|
+
* The request is spelled `provider/model` — the same shape
|
|
27
|
+
* `effectiveSettings.model` reports the configured model in — and splits at
|
|
28
|
+
* the FIRST slash, so a provider-qualified id survives a model id that
|
|
29
|
+
* contains none. A request with no slash names the MODEL only and keeps the
|
|
30
|
+
* default selection's provider, which is what a caller switching between two
|
|
31
|
+
* models of the same route means.
|
|
32
|
+
*
|
|
33
|
+
* Everything else on the selection is carried through untouched (the
|
|
34
|
+
* reasoning effort in particular): `--model` changes the model, not the rest
|
|
35
|
+
* of the instance's configuration. An absent or blank request returns the
|
|
36
|
+
* default selection unchanged — byte for byte the behavior before the flag
|
|
37
|
+
* existed.
|
|
38
|
+
* @param selection - the instance's default model selection.
|
|
39
|
+
* @param request - the `--model` value, when the launch carried one.
|
|
40
|
+
* @returns the selection this launch runs.
|
|
41
|
+
*/
|
|
42
|
+
export declare function applyModelRequest(selection: ModelSelection, request: string | undefined): ModelSelection;
|
|
43
|
+
/**
|
|
44
|
+
* Join this agent to the sub-profile's preset, when the sub-profile composes
|
|
45
|
+
* a roster.
|
|
46
|
+
*
|
|
47
|
+
* Without a roster this is a no-op and the agent reads the model-facing rows
|
|
48
|
+
* off the global layer — byte for byte the behavior of every sub-dsh before
|
|
49
|
+
* the roster layer existed, and the right answer for a rosterless
|
|
50
|
+
* composition (`@deepseek-ai/dsh-agent-presets` README, "Composing a child
|
|
51
|
+
* agent"). With one, the preset decides the tools, prompt sections and
|
|
52
|
+
* skills of every session this process hosts, which is what makes the
|
|
53
|
+
* scope's capability face a per-condition factor instead of an instance-wide
|
|
54
|
+
* constant.
|
|
55
|
+
*
|
|
56
|
+
* A roster that REFUSES (unknown preset id, broken composition) is not
|
|
57
|
+
* degraded past: mounting rejects, the agent creation rolls back, and the
|
|
58
|
+
* launch fails naming the preset. A sub-dsh that silently ran the global
|
|
59
|
+
* layer after being told to run preset X would attribute the round to a
|
|
60
|
+
* capability face it never had.
|
|
61
|
+
* @param ctx - plugin context (where the roster service is resolved).
|
|
62
|
+
* @param agentCtx - the agent's scope context, which joins the standing mount.
|
|
63
|
+
* @returns the preset id joined, or undefined when the composition has no roster.
|
|
64
|
+
*/
|
|
65
|
+
export declare function joinSubDshPreset(ctx: Context, agentCtx: Context): Promise<string | undefined>;
|
|
66
|
+
/**
|
|
67
|
+
* Create or resume the direct Agent for one caller-named session. The caller
|
|
68
|
+
* resolved the core services first (both call sites bail out on a disposed
|
|
69
|
+
* tree). The caller-supplied id is authoritative for both branches: the
|
|
70
|
+
* parent provider generated one uuid and passes the same value on every round
|
|
71
|
+
* of the same delegation, so resume finds exactly the sub-dsh session the
|
|
72
|
+
* fresh round created.
|
|
73
|
+
* @param ctx - plugin context carrying the Agent registry and default model.
|
|
74
|
+
* @param identity - fresh (`sessionId`) or continuation (`resumeSessionId`).
|
|
75
|
+
* @returns the owned agent handle (its disposer unwinds the agent's world).
|
|
76
|
+
*/
|
|
77
|
+
export declare function loadSubDshAgent(ctx: Context, identity: {
|
|
78
|
+
sessionId?: string;
|
|
79
|
+
resumeSessionId?: string;
|
|
80
|
+
model?: string;
|
|
81
|
+
effort?: string;
|
|
82
|
+
}): Promise<AgentHandle>;
|
|
83
|
+
/** Validate the actual sub-instance's configuration without creating a session or generating. */
|
|
84
|
+
export declare function resolveSubDshSelection(ctx: Context, model?: string, effort?: string, validateModel?: boolean): Promise<ModelSelection>;
|
|
85
|
+
//# sourceMappingURL=agent-loader.d.ts.map
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent loading and turn aggregation shared by the one-shot runner and the
|
|
3
|
+
* serve loop: both drive a direct Agent over the core registry with the same
|
|
4
|
+
* model selection, the same scoped installModelSelection setup, the same
|
|
5
|
+
* preset join, and the same caller-supplied session identity, differing only
|
|
6
|
+
* in process lifetime.
|
|
7
|
+
* @module @khorsheed/dsh-local-agent-dsh-headless/agent-loader
|
|
8
|
+
*/
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import { installModelSelection } from '@deepseek-ai/dsh-agent';
|
|
11
|
+
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
12
|
+
/**
|
|
13
|
+
* Aggregate the last assistant text and turn outcome in one owned interval.
|
|
14
|
+
* @param events - the agent session's full event log.
|
|
15
|
+
* @param firstSeq - the session seq at which this driver's interval opened.
|
|
16
|
+
*/
|
|
17
|
+
export function summarizeTurn(events, firstSeq) {
|
|
18
|
+
let started = false;
|
|
19
|
+
let text = '';
|
|
20
|
+
let reason;
|
|
21
|
+
for (const event of events) {
|
|
22
|
+
if (event.seq < firstSeq)
|
|
23
|
+
continue;
|
|
24
|
+
if (event.type === 'turn/start') {
|
|
25
|
+
started = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (!started)
|
|
29
|
+
continue;
|
|
30
|
+
if (event.type === 'assistant/message') {
|
|
31
|
+
const joined = event.data.message.content
|
|
32
|
+
.filter(block => block.type === 'text')
|
|
33
|
+
.map(block => block.text)
|
|
34
|
+
.join('');
|
|
35
|
+
if (joined !== '')
|
|
36
|
+
text = joined;
|
|
37
|
+
}
|
|
38
|
+
if (event.type === 'turn/end')
|
|
39
|
+
reason = event.data.reason;
|
|
40
|
+
}
|
|
41
|
+
return { text, reason };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Apply a `--model` request onto the instance's default selection.
|
|
45
|
+
*
|
|
46
|
+
* The request is spelled `provider/model` — the same shape
|
|
47
|
+
* `effectiveSettings.model` reports the configured model in — and splits at
|
|
48
|
+
* the FIRST slash, so a provider-qualified id survives a model id that
|
|
49
|
+
* contains none. A request with no slash names the MODEL only and keeps the
|
|
50
|
+
* default selection's provider, which is what a caller switching between two
|
|
51
|
+
* models of the same route means.
|
|
52
|
+
*
|
|
53
|
+
* Everything else on the selection is carried through untouched (the
|
|
54
|
+
* reasoning effort in particular): `--model` changes the model, not the rest
|
|
55
|
+
* of the instance's configuration. An absent or blank request returns the
|
|
56
|
+
* default selection unchanged — byte for byte the behavior before the flag
|
|
57
|
+
* existed.
|
|
58
|
+
* @param selection - the instance's default model selection.
|
|
59
|
+
* @param request - the `--model` value, when the launch carried one.
|
|
60
|
+
* @returns the selection this launch runs.
|
|
61
|
+
*/
|
|
62
|
+
export function applyModelRequest(selection, request) {
|
|
63
|
+
const wanted = request?.trim();
|
|
64
|
+
if (wanted === undefined || wanted === '')
|
|
65
|
+
return selection;
|
|
66
|
+
const slash = wanted.indexOf('/');
|
|
67
|
+
if (slash <= 0 || slash === wanted.length - 1)
|
|
68
|
+
return { ...selection, model: wanted };
|
|
69
|
+
return { ...selection, provider: wanted.slice(0, slash), model: wanted.slice(slash + 1) };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Join this agent to the sub-profile's preset, when the sub-profile composes
|
|
73
|
+
* a roster.
|
|
74
|
+
*
|
|
75
|
+
* Without a roster this is a no-op and the agent reads the model-facing rows
|
|
76
|
+
* off the global layer — byte for byte the behavior of every sub-dsh before
|
|
77
|
+
* the roster layer existed, and the right answer for a rosterless
|
|
78
|
+
* composition (`@deepseek-ai/dsh-agent-presets` README, "Composing a child
|
|
79
|
+
* agent"). With one, the preset decides the tools, prompt sections and
|
|
80
|
+
* skills of every session this process hosts, which is what makes the
|
|
81
|
+
* scope's capability face a per-condition factor instead of an instance-wide
|
|
82
|
+
* constant.
|
|
83
|
+
*
|
|
84
|
+
* A roster that REFUSES (unknown preset id, broken composition) is not
|
|
85
|
+
* degraded past: mounting rejects, the agent creation rolls back, and the
|
|
86
|
+
* launch fails naming the preset. A sub-dsh that silently ran the global
|
|
87
|
+
* layer after being told to run preset X would attribute the round to a
|
|
88
|
+
* capability face it never had.
|
|
89
|
+
* @param ctx - plugin context (where the roster service is resolved).
|
|
90
|
+
* @param agentCtx - the agent's scope context, which joins the standing mount.
|
|
91
|
+
* @returns the preset id joined, or undefined when the composition has no roster.
|
|
92
|
+
*/
|
|
93
|
+
export async function joinSubDshPreset(ctx, agentCtx) {
|
|
94
|
+
const roster = ctx.get('agentPresets');
|
|
95
|
+
if (roster?.mount === undefined)
|
|
96
|
+
return undefined;
|
|
97
|
+
return (await roster.mount(agentCtx)).id;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Create or resume the direct Agent for one caller-named session. The caller
|
|
101
|
+
* resolved the core services first (both call sites bail out on a disposed
|
|
102
|
+
* tree). The caller-supplied id is authoritative for both branches: the
|
|
103
|
+
* parent provider generated one uuid and passes the same value on every round
|
|
104
|
+
* of the same delegation, so resume finds exactly the sub-dsh session the
|
|
105
|
+
* fresh round created.
|
|
106
|
+
* @param ctx - plugin context carrying the Agent registry and default model.
|
|
107
|
+
* @param identity - fresh (`sessionId`) or continuation (`resumeSessionId`).
|
|
108
|
+
* @returns the owned agent handle (its disposer unwinds the agent's world).
|
|
109
|
+
*/
|
|
110
|
+
export async function loadSubDshAgent(ctx, identity) {
|
|
111
|
+
const agents = ctx.get('agents');
|
|
112
|
+
if (agents === undefined)
|
|
113
|
+
throw new Error('local-agent-dsh-headless: the agents service is not mounted');
|
|
114
|
+
const selection = await resolveSubDshSelection(ctx, identity.model, identity.effort);
|
|
115
|
+
// This bundle composes no preset roster, so the model-facing rows sit in the
|
|
116
|
+
// host plane and the agent reads them from the global layer. A deployment
|
|
117
|
+
// that DOES configure one has to join it here first
|
|
118
|
+
// (@deepseek-ai/dsh-agent-presets README, "Composing a child agent").
|
|
119
|
+
const agentOptions = { provider: selection.provider, model: selection.model };
|
|
120
|
+
const setup = async (agentCtx) => {
|
|
121
|
+
const selected = { current: selection, assembled: undefined };
|
|
122
|
+
// The released installModelSelection registers the two scoped waterfall
|
|
123
|
+
// listeners and returns their disposer; the run owns the whole process
|
|
124
|
+
// lifetime, so the disposer is deliberately dropped.
|
|
125
|
+
installModelSelection(agentCtx, selected);
|
|
126
|
+
// The preset join happens INSIDE setup, before the agent is published:
|
|
127
|
+
// the roster parents this agent's scope key to the preset's standing
|
|
128
|
+
// mount, so the preset's tools and prompt sections exist before the first
|
|
129
|
+
// prompt assembly. A rejection here rolls the creation back.
|
|
130
|
+
await joinSubDshPreset(ctx, agentCtx);
|
|
131
|
+
};
|
|
132
|
+
if (identity.resumeSessionId !== undefined) {
|
|
133
|
+
return agents.resume({
|
|
134
|
+
resumeSessionId: SessionId(identity.resumeSessionId),
|
|
135
|
+
agentOptions,
|
|
136
|
+
setup,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return agents.create({
|
|
140
|
+
sessionId: SessionId(identity.sessionId ?? `session-${randomUUID()}`),
|
|
141
|
+
meta: { cwd: process.cwd() },
|
|
142
|
+
agentOptions,
|
|
143
|
+
setup,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/** Validate the actual sub-instance's configuration without creating a session or generating. */
|
|
147
|
+
export async function resolveSubDshSelection(ctx, model, effort, validateModel = false) {
|
|
148
|
+
const defaults = ctx.get('agentDefaultModel');
|
|
149
|
+
if (defaults === undefined)
|
|
150
|
+
throw new Error('local-agent-dsh-headless: agentDefaultModel is not mounted');
|
|
151
|
+
const selection = applyModelRequest(defaults.currentSelection(), model);
|
|
152
|
+
if (effort === undefined && !validateModel)
|
|
153
|
+
return selection;
|
|
154
|
+
let timer;
|
|
155
|
+
try {
|
|
156
|
+
const info = await Promise.race([
|
|
157
|
+
ctx.get('llm')?.resolveModelInfo(selection.provider, selection.model, AbortSignal.timeout(5_000)),
|
|
158
|
+
new Promise((_resolve, reject) => { timer = setTimeout(() => reject(new Error('Sub-DSH model validation timed out')), 5_000); timer.unref(); }),
|
|
159
|
+
]);
|
|
160
|
+
if (info === undefined)
|
|
161
|
+
throw new Error('The sub-DSH adapter cannot resolve the requested model');
|
|
162
|
+
if (effort === undefined)
|
|
163
|
+
return selection;
|
|
164
|
+
const native = info.reasoning?.efforts.find(option => String(option.id) === effort);
|
|
165
|
+
if (native === undefined)
|
|
166
|
+
throw new Error('The sub-DSH model does not advertise the requested reasoning effort');
|
|
167
|
+
return { ...selection, reasoningEffort: native.id };
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
if (timer !== undefined)
|
|
171
|
+
clearTimeout(timer);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @khorsheed/dsh-local-agent-dsh-headless — sub-dsh direct Agent driver with
|
|
3
|
+
* two modes. One-shot (default): create one Agent through the core registry
|
|
4
|
+
* on a caller-supplied session id (or resume one), drive the task to
|
|
5
|
+
* quiescence, flush its Session, print the final assistant text, and exit.
|
|
6
|
+
* Serve (`--serve`): stay resident and drive turns over the family-internal
|
|
7
|
+
* live-driver wire (see `./serve.ts` / `./wire.ts`). The session id comes
|
|
8
|
+
* from the invocation (`--session-id` / `--resume`) or the wire, never from
|
|
9
|
+
* stdout — the parent provider generates one uuid and both sides use it, so
|
|
10
|
+
* stdout carries no parseable session marker.
|
|
11
|
+
*
|
|
12
|
+
* @module @khorsheed/dsh-local-agent-dsh-headless
|
|
13
|
+
*/
|
|
14
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
15
|
+
import z from '@deepseek-ai/schemastery';
|
|
16
|
+
/** Stable Cordis plugin name. */
|
|
17
|
+
export declare const name = "local-agent-dsh-headless-runner";
|
|
18
|
+
/** Core services required before the one-shot turn can start. */
|
|
19
|
+
export declare const inject: string[];
|
|
20
|
+
/** Plugin config: the one-shot task and caller session identity, or serve mode. */
|
|
21
|
+
export interface Config {
|
|
22
|
+
/** The prompt text for the single run; unused in serve mode. */
|
|
23
|
+
task?: string;
|
|
24
|
+
/** Fresh delegation: create a session with exactly this id. */
|
|
25
|
+
sessionId?: string;
|
|
26
|
+
/** Continuation: resume the existing session with this id. */
|
|
27
|
+
resumeSessionId?: string;
|
|
28
|
+
/** Resident mode: drive turns over the stdio wire instead of one task. */
|
|
29
|
+
serve?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The model this launch runs (`--model`), spelled `provider/model`; a bare
|
|
32
|
+
* id names the model and keeps the instance's provider. Absent means the
|
|
33
|
+
* instance's default selection, unchanged. Under `serve` it binds every
|
|
34
|
+
* session the resident process hosts.
|
|
35
|
+
*/
|
|
36
|
+
model?: string;
|
|
37
|
+
effort?: string;
|
|
38
|
+
}
|
|
39
|
+
export declare const Config: z<Config>;
|
|
40
|
+
/** Process-facing effects of one run: output streams plus the launcher's bounded exit request. */
|
|
41
|
+
interface HeadlessIo {
|
|
42
|
+
stdin: {
|
|
43
|
+
on(event: 'data', listener: (chunk: unknown) => void): unknown;
|
|
44
|
+
on(event: 'end', listener: () => void): unknown;
|
|
45
|
+
};
|
|
46
|
+
stdout: {
|
|
47
|
+
write(chunk: string): unknown;
|
|
48
|
+
};
|
|
49
|
+
stderr: {
|
|
50
|
+
write(chunk: string): unknown;
|
|
51
|
+
};
|
|
52
|
+
/** Request process exit with `code` after the tree disposes. */
|
|
53
|
+
exit(code: number): void;
|
|
54
|
+
}
|
|
55
|
+
/** The process streams the runner writes to; tests substitute captures. */
|
|
56
|
+
export declare const internals: {
|
|
57
|
+
stdin: HeadlessIo['stdin'];
|
|
58
|
+
stdout: HeadlessIo['stdout'];
|
|
59
|
+
stderr: HeadlessIo['stderr'];
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Mount the direct driver: one-shot by default, the resident serve loop when
|
|
63
|
+
* the invocation passed `--serve`.
|
|
64
|
+
* @param ctx - plugin context carrying core services and the launcher-provided exit request.
|
|
65
|
+
* @param config - validated task/serve config.
|
|
66
|
+
*/
|
|
67
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
68
|
+
export {};
|
|
69
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @khorsheed/dsh-local-agent-dsh-headless — sub-dsh direct Agent driver with
|
|
3
|
+
* two modes. One-shot (default): create one Agent through the core registry
|
|
4
|
+
* on a caller-supplied session id (or resume one), drive the task to
|
|
5
|
+
* quiescence, flush its Session, print the final assistant text, and exit.
|
|
6
|
+
* Serve (`--serve`): stay resident and drive turns over the family-internal
|
|
7
|
+
* live-driver wire (see `./serve.ts` / `./wire.ts`). The session id comes
|
|
8
|
+
* from the invocation (`--session-id` / `--resume`) or the wire, never from
|
|
9
|
+
* stdout — the parent provider generates one uuid and both sides use it, so
|
|
10
|
+
* stdout carries no parseable session marker.
|
|
11
|
+
*
|
|
12
|
+
* @module @khorsheed/dsh-local-agent-dsh-headless
|
|
13
|
+
*/
|
|
14
|
+
import z from '@deepseek-ai/schemastery';
|
|
15
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
16
|
+
import { loadSubDshAgent, summarizeTurn } from './agent-loader.js';
|
|
17
|
+
import { runServe } from './serve.js';
|
|
18
|
+
/** Stable Cordis plugin name. */
|
|
19
|
+
export const name = 'local-agent-dsh-headless-runner';
|
|
20
|
+
/** Core services required before the one-shot turn can start. */
|
|
21
|
+
export const inject = ['agentDefaultModel', 'agents', 'sessions'];
|
|
22
|
+
export const Config = z.object({
|
|
23
|
+
task: z.string(),
|
|
24
|
+
sessionId: z.string(),
|
|
25
|
+
resumeSessionId: z.string(),
|
|
26
|
+
serve: z.boolean().default(false),
|
|
27
|
+
model: z.string(),
|
|
28
|
+
effort: z.string(),
|
|
29
|
+
});
|
|
30
|
+
/** The process streams the runner writes to; tests substitute captures. */
|
|
31
|
+
export const internals = {
|
|
32
|
+
stdin: process.stdin,
|
|
33
|
+
stdout: process.stdout,
|
|
34
|
+
stderr: process.stderr,
|
|
35
|
+
};
|
|
36
|
+
/** Report an unexpected direct-driver failure and request a failing exit. */
|
|
37
|
+
function fail(io, error) {
|
|
38
|
+
io.stderr.write(`dsh: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
39
|
+
io.exit(1);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Run one task through a fresh or resumed Agent and request process exit.
|
|
43
|
+
* @param ctx - plugin context carrying the Agent, default model, Session, and launcher IO services.
|
|
44
|
+
* @param config - validated one-shot config (task plus the caller session id).
|
|
45
|
+
* @param io - process-facing effects.
|
|
46
|
+
*/
|
|
47
|
+
async function run(ctx, config, io) {
|
|
48
|
+
// Loader siblings mount concurrently. Await the complete application before
|
|
49
|
+
// creating an Agent so its scoped tools and adapters are not half-composed.
|
|
50
|
+
await ctx.get('loader')?.await();
|
|
51
|
+
const sessions = ctx.get('sessions');
|
|
52
|
+
// Early process shutdown can dispose the tree while settlement is pending.
|
|
53
|
+
if (ctx.get('agents') === undefined || ctx.get('agentDefaultModel') === undefined || sessions === undefined)
|
|
54
|
+
return;
|
|
55
|
+
const handle = await loadSubDshAgent(ctx, {
|
|
56
|
+
...config.sessionId === undefined ? {} : { sessionId: config.sessionId },
|
|
57
|
+
...config.resumeSessionId === undefined ? {} : { resumeSessionId: config.resumeSessionId },
|
|
58
|
+
...config.model === undefined ? {} : { model: config.model },
|
|
59
|
+
...config.effort === undefined ? {} : { effort: config.effort },
|
|
60
|
+
});
|
|
61
|
+
const agent = handle.agent;
|
|
62
|
+
await agent.whenIdle();
|
|
63
|
+
const firstSeq = agent.session.seq;
|
|
64
|
+
agent.followup(createUserMessage({
|
|
65
|
+
content: [{ type: 'text', text: config.task ?? '' }],
|
|
66
|
+
source: { kind: 'user' },
|
|
67
|
+
}));
|
|
68
|
+
await agent.whenIdle();
|
|
69
|
+
await sessions.flush(agent.session);
|
|
70
|
+
const outcome = summarizeTurn(agent.session.snapshotEvents(), firstSeq);
|
|
71
|
+
io.stdout.write(outcome.text + '\n');
|
|
72
|
+
if (outcome.reason?.kind === 'error') {
|
|
73
|
+
io.stderr.write(`dsh: ${outcome.reason.error.code}: ${outcome.reason.error.message}\n`);
|
|
74
|
+
}
|
|
75
|
+
io.exit(outcome.reason?.kind === 'completed' ? 0 : 1);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Mount the direct driver: one-shot by default, the resident serve loop when
|
|
79
|
+
* the invocation passed `--serve`.
|
|
80
|
+
* @param ctx - plugin context carrying core services and the launcher-provided exit request.
|
|
81
|
+
* @param config - validated task/serve config.
|
|
82
|
+
*/
|
|
83
|
+
export function apply(ctx, config) {
|
|
84
|
+
// Read through the global service store, not the property proxy: appExit is
|
|
85
|
+
// an optional host value, never an injected dependency.
|
|
86
|
+
const exit = ctx.get('appExit');
|
|
87
|
+
if (exit === undefined) {
|
|
88
|
+
throw new Error('local-agent-dsh-headless-runner: the launcher must provide ctx.appExit before the tree mounts');
|
|
89
|
+
}
|
|
90
|
+
const io = { stdin: internals.stdin, stdout: internals.stdout, stderr: internals.stderr, exit };
|
|
91
|
+
if (config.serve === true) {
|
|
92
|
+
void runServe(ctx, io, config.model, config.effort).catch((error) => { fail(io, error); });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (config.task === undefined || config.task.trim() === '') {
|
|
96
|
+
throw new Error('local-agent-dsh-headless-runner: a task is required unless serve mode is on');
|
|
97
|
+
}
|
|
98
|
+
void run(ctx, config, io).catch((error) => { fail(io, error); });
|
|
99
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@khorsheed/dsh-local-agent-dsh-headless`.
|
|
3
|
+
* @module @khorsheed/dsh-local-agent-dsh-headless/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "local-agent-dsh-headless-invariant";
|
|
8
|
+
/** Service required before the companion can register. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@khorsheed/dsh-local-agent-dsh-headless`.
|
|
3
|
+
* @module @khorsheed/dsh-local-agent-dsh-headless/invariant
|
|
4
|
+
*/
|
|
5
|
+
const PACKAGE_NAME = '@khorsheed/dsh-local-agent-dsh-headless';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export const name = 'local-agent-dsh-headless-invariant';
|
|
8
|
+
/** Service required before the companion can register. */
|
|
9
|
+
export const inject = ['invariants'];
|
|
10
|
+
/**
|
|
11
|
+
* Composition guard: this bundle is sub-profile-only. Its patch rows
|
|
12
|
+
* (persona override, `hmr` off, `tools` mode, the `ptc-runtime` insert, the
|
|
13
|
+
* member-bridge MCP row) collide with or leak into any Host/web composition —
|
|
14
|
+
* 2026-08-23 P0: reconciled into the prod web profile as a direct dependency
|
|
15
|
+
* (reconcilePlugins auto-mounts every `dsh.bundle`-declaring direct dep), the
|
|
16
|
+
* `code-runtime` insert (today's `ptc-runtime` row) hit the web-app row's duplicate entry id and the
|
|
17
|
+
* whole instance failed to boot. The loader's duplicate-id failure is loud
|
|
18
|
+
* but cryptic, and only fires when the ids collide; this invariant is the
|
|
19
|
+
* CLEAR failure for the non-colliding case (e.g. an upstream row rename would
|
|
20
|
+
* otherwise leak the persona override into real user sessions). The web
|
|
21
|
+
* layer's `webStartup` service is the marker: in-box bundles mount before
|
|
22
|
+
* dependency-managed ones, so it is already provided when this companion
|
|
23
|
+
* applies.
|
|
24
|
+
*/
|
|
25
|
+
const install = (ctx, fail) => {
|
|
26
|
+
// Duck-typed global-store read: the web marker is not a dependency of this
|
|
27
|
+
// package, so no typed Context merge exists for it.
|
|
28
|
+
const probe = ctx;
|
|
29
|
+
if (probe.get('webStartup') !== undefined) {
|
|
30
|
+
fail('mounted into a web composition — this bundle is sub-profile-only. '
|
|
31
|
+
+ 'Remove it from the profile’s direct dependencies: the parent '
|
|
32
|
+
+ 'local-agent-dsh bundle installs it transitively, and the '
|
|
33
|
+
+ 'sub-profile symlink resolves from that closure');
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Register this package's invariant companion.
|
|
38
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
39
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
40
|
+
*/
|
|
41
|
+
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
@@ -0,0 +1,39 @@
|
|
|
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 type { Context } from '@deepseek-ai/cordis';
|
|
14
|
+
/**
|
|
15
|
+
* The process streams serve mode reads and writes; tests substitute captures.
|
|
16
|
+
* `stdin` is the minimal readable surface (`data`/`end`), `exit` is the
|
|
17
|
+
* launcher-provided bounded exit request.
|
|
18
|
+
*/
|
|
19
|
+
export interface ServeIo {
|
|
20
|
+
readonly stdin: {
|
|
21
|
+
on(event: 'data', listener: (chunk: unknown) => void): unknown;
|
|
22
|
+
on(event: 'end', listener: () => void): unknown;
|
|
23
|
+
};
|
|
24
|
+
readonly stdout: {
|
|
25
|
+
write(chunk: string): unknown;
|
|
26
|
+
};
|
|
27
|
+
readonly stderr: {
|
|
28
|
+
write(chunk: string): unknown;
|
|
29
|
+
};
|
|
30
|
+
readonly exit: (code: number) => void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Run the resident loop until stdin closes or a `shutdown` request arrives.
|
|
34
|
+
* @param ctx - plugin context carrying the loader settle, core services, and
|
|
35
|
+
* the session/event feed.
|
|
36
|
+
* @param io - process-facing effects.
|
|
37
|
+
*/
|
|
38
|
+
export declare function runServe(ctx: Context, io: ServeIo, model?: string, effort?: string): Promise<void>;
|
|
39
|
+
//# sourceMappingURL=serve.d.ts.map
|