dsh-agy-cli 0.0.0-stage → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +30 -0
- package/README.en.md +27 -0
- package/README.md +94 -2
- package/agents/dsh-agy-image-view.md +17 -0
- package/agents/dsh-agy-mcp-executor.md +32 -0
- package/agents/dsh-agy-read-only.md +20 -0
- package/agents/dsh-agy-tool-free.md +16 -0
- package/agents/dsh-agy-workspace-write.md +23 -0
- package/bin/dsh-agy-cli.js +9 -0
- package/bin/win32-x64/agy-launcher.exe +0 -0
- package/cordis.patch.yml +16 -0
- package/dist/client.js +4259 -0
- package/lib/agent-installer.d.ts +66 -0
- package/lib/agent-installer.js +147 -0
- package/lib/agent-presets.d.ts +42 -0
- package/lib/agent-presets.js +130 -0
- package/lib/agents-cli.d.ts +3 -0
- package/lib/agents-cli.js +110 -0
- package/lib/agy/diagnostics.d.ts +29 -0
- package/lib/agy/diagnostics.js +109 -0
- package/lib/agy/errors.d.ts +20 -0
- package/lib/agy/errors.js +77 -0
- package/lib/agy/experimental-transport.d.ts +79 -0
- package/lib/agy/experimental-transport.js +612 -0
- package/lib/agy/keepalive.d.ts +140 -0
- package/lib/agy/keepalive.js +424 -0
- package/lib/agy/limiter.d.ts +32 -0
- package/lib/agy/limiter.js +111 -0
- package/lib/agy/log.d.ts +214 -0
- package/lib/agy/log.js +503 -0
- package/lib/agy/models.d.ts +49 -0
- package/lib/agy/models.js +203 -0
- package/lib/agy/overrides.d.ts +17 -0
- package/lib/agy/overrides.js +73 -0
- package/lib/agy/parser.d.ts +102 -0
- package/lib/agy/parser.js +492 -0
- package/lib/agy/persistent-transport.d.ts +101 -0
- package/lib/agy/persistent-transport.js +753 -0
- package/lib/agy/process.d.ts +144 -0
- package/lib/agy/process.js +452 -0
- package/lib/agy/redact.d.ts +3 -0
- package/lib/agy/redact.js +24 -0
- package/lib/agy/stream-protocol.d.ts +50 -0
- package/lib/agy/stream-protocol.js +156 -0
- package/lib/agy/windows-launcher.d.ts +21 -0
- package/lib/agy/windows-launcher.js +76 -0
- package/lib/diagnostics.d.ts +78 -0
- package/lib/diagnostics.js +162 -0
- package/lib/doctor.d.ts +138 -0
- package/lib/doctor.js +923 -0
- package/lib/dsh/context.d.ts +133 -0
- package/lib/dsh/context.js +352 -0
- package/lib/dsh-home.d.ts +6 -0
- package/lib/dsh-home.js +11 -0
- package/lib/index.d.ts +43 -0
- package/lib/index.js +256 -0
- package/lib/mcp/agy-registration.d.ts +70 -0
- package/lib/mcp/agy-registration.js +245 -0
- package/lib/mcp/bridge.d.ts +73 -0
- package/lib/mcp/bridge.js +295 -0
- package/lib/mcp/endpoint.d.ts +41 -0
- package/lib/mcp/endpoint.js +142 -0
- package/lib/mcp/protocol.d.ts +107 -0
- package/lib/mcp/protocol.js +197 -0
- package/lib/mcp/tool-projection.d.ts +64 -0
- package/lib/mcp/tool-projection.js +168 -0
- package/lib/provider/agy.d.ts +153 -0
- package/lib/provider/agy.js +2159 -0
- package/lib/provider/config.d.ts +226 -0
- package/lib/provider/config.js +294 -0
- package/lib/provider/error-codes.d.ts +22 -0
- package/lib/provider/error-codes.js +29 -0
- package/lib/provider/image-bridge.d.ts +60 -0
- package/lib/provider/image-bridge.js +290 -0
- package/lib/provider/mock.d.ts +15 -0
- package/lib/provider/mock.js +78 -0
- package/lib/provider/prompt-budget.d.ts +45 -0
- package/lib/provider/prompt-budget.js +596 -0
- package/lib/provider/serialize.d.ts +15 -0
- package/lib/provider/serialize.js +82 -0
- package/lib/provider/tool-protocol.d.ts +80 -0
- package/lib/provider/tool-protocol.js +848 -0
- package/lib/provider/tool-schema-file.d.ts +11 -0
- package/lib/provider/tool-schema-file.js +27 -0
- package/lib/session/file-store.d.ts +56 -0
- package/lib/session/file-store.js +186 -0
- package/lib/session/store.d.ts +49 -0
- package/lib/session/store.js +62 -0
- package/lib/web/router.d.ts +19 -0
- package/lib/web/router.js +906 -0
- package/package.json +112 -4
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal MCP (Model Context Protocol) server-side JSON-RPC layer.
|
|
3
|
+
*
|
|
4
|
+
* DSH ships `@modelcontextprotocol/core`, but that package exports only Zod
|
|
5
|
+
* schemas — it has no `Server` class and no transport. The framing and the
|
|
6
|
+
* three methods AGY actually needs are implemented here, deliberately small:
|
|
7
|
+
* a session-based server (one session per AGY conversation) over newline
|
|
8
|
+
* delimited JSON-RPC 2.0.
|
|
9
|
+
*
|
|
10
|
+
* Wire compatibility notes learned from AGY 1.3.2:
|
|
11
|
+
* - the negotiated protocol version is echoed back from the client, because
|
|
12
|
+
* AGY rejects a server that answers with a version it did not offer;
|
|
13
|
+
* - a JSON-RPC *notification* (no `id`) must never be answered;
|
|
14
|
+
* - tool failures travel as a normal `tools/call` result with `isError: true`
|
|
15
|
+
* so the model can react to them instead of seeing a transport error.
|
|
16
|
+
*/
|
|
17
|
+
export const MCP_PROTOCOL_VERSION = '2025-06-18';
|
|
18
|
+
export const MCP_SUPPORTED_PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'];
|
|
19
|
+
export const MCP_ERROR_CODES = Object.freeze({
|
|
20
|
+
parseError: -32700,
|
|
21
|
+
invalidRequest: -32600,
|
|
22
|
+
methodNotFound: -32601,
|
|
23
|
+
invalidParams: -32602,
|
|
24
|
+
internalError: -32603,
|
|
25
|
+
});
|
|
26
|
+
function isRecord(value) {
|
|
27
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
function errorMessage(error) {
|
|
30
|
+
return error instanceof Error ? error.message : String(error);
|
|
31
|
+
}
|
|
32
|
+
/** Pick the highest protocol version both sides understand. */
|
|
33
|
+
export function negotiateProtocolVersion(requested) {
|
|
34
|
+
if (typeof requested === 'string' && MCP_SUPPORTED_PROTOCOL_VERSIONS.includes(requested)) {
|
|
35
|
+
return requested;
|
|
36
|
+
}
|
|
37
|
+
return MCP_PROTOCOL_VERSION;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* One MCP server instance. It is transport agnostic: `handleMessage` takes one
|
|
41
|
+
* decoded JSON-RPC message and returns the response to write back, or
|
|
42
|
+
* `undefined` for notifications.
|
|
43
|
+
*/
|
|
44
|
+
export class McpServerSession {
|
|
45
|
+
handlers;
|
|
46
|
+
initialized = false;
|
|
47
|
+
clientInfo;
|
|
48
|
+
sessionHandle = '';
|
|
49
|
+
constructor(handlers) {
|
|
50
|
+
this.handlers = handlers;
|
|
51
|
+
}
|
|
52
|
+
get isInitialized() {
|
|
53
|
+
return this.initialized;
|
|
54
|
+
}
|
|
55
|
+
get client() {
|
|
56
|
+
return this.clientInfo;
|
|
57
|
+
}
|
|
58
|
+
async handleMessage(raw, sessionHandle = '') {
|
|
59
|
+
this.sessionHandle = sessionHandle;
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(raw);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// JSON-RPC 2.0: an unparseable body has no recoverable id, so the answer
|
|
66
|
+
// carries `id: null`, not a fabricated 0.
|
|
67
|
+
return {
|
|
68
|
+
jsonrpc: '2.0',
|
|
69
|
+
id: null,
|
|
70
|
+
error: { code: MCP_ERROR_CODES.parseError, message: 'Parse error: body is not valid JSON' },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (!isRecord(parsed) || parsed.jsonrpc !== '2.0' || typeof parsed.method !== 'string') {
|
|
74
|
+
return {
|
|
75
|
+
jsonrpc: '2.0',
|
|
76
|
+
id: isRecord(parsed) && (typeof parsed.id === 'string' || typeof parsed.id === 'number') ? parsed.id : null,
|
|
77
|
+
error: { code: MCP_ERROR_CODES.invalidRequest, message: 'Invalid Request' },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const id = parsed.id;
|
|
81
|
+
const isRequest = typeof id === 'string' || typeof id === 'number';
|
|
82
|
+
try {
|
|
83
|
+
const result = await this.dispatch(parsed.method, isRecord(parsed.params) ? parsed.params : {});
|
|
84
|
+
// Notifications carry no id and must not be answered.
|
|
85
|
+
if (!isRequest)
|
|
86
|
+
return undefined;
|
|
87
|
+
if (result === NOTIFICATION)
|
|
88
|
+
return undefined;
|
|
89
|
+
return { jsonrpc: '2.0', id, result };
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (!isRequest)
|
|
93
|
+
return undefined;
|
|
94
|
+
if (error instanceof McpRpcError) {
|
|
95
|
+
return { jsonrpc: '2.0', id, error: { code: error.code, message: error.message, ...(error.data === undefined ? {} : { data: error.data }) } };
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
jsonrpc: '2.0',
|
|
99
|
+
id,
|
|
100
|
+
error: { code: MCP_ERROR_CODES.internalError, message: errorMessage(error) },
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async dispatch(method, params) {
|
|
105
|
+
switch (method) {
|
|
106
|
+
case 'initialize':
|
|
107
|
+
return this.initialize(params);
|
|
108
|
+
case 'notifications/initialized':
|
|
109
|
+
case 'notifications/cancelled':
|
|
110
|
+
case 'notifications/roots/list_changed':
|
|
111
|
+
return NOTIFICATION;
|
|
112
|
+
case 'ping':
|
|
113
|
+
return {};
|
|
114
|
+
case 'tools/list':
|
|
115
|
+
return { tools: await this.handlers.listTools(this.resolveSession(params)) };
|
|
116
|
+
case 'tools/call':
|
|
117
|
+
return this.callTool(params);
|
|
118
|
+
default:
|
|
119
|
+
throw new McpRpcError(MCP_ERROR_CODES.methodNotFound, `Method not found: ${method}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
initialize(params) {
|
|
123
|
+
this.initialized = true;
|
|
124
|
+
const clientInfo = params.clientInfo;
|
|
125
|
+
this.clientInfo = isRecord(clientInfo)
|
|
126
|
+
? {
|
|
127
|
+
...(typeof clientInfo.name === 'string' ? { name: clientInfo.name } : {}),
|
|
128
|
+
...(typeof clientInfo.version === 'string' ? { version: clientInfo.version } : {}),
|
|
129
|
+
}
|
|
130
|
+
: undefined;
|
|
131
|
+
return {
|
|
132
|
+
protocolVersion: negotiateProtocolVersion(params.protocolVersion),
|
|
133
|
+
capabilities: { tools: { listChanged: false } },
|
|
134
|
+
serverInfo: { name: this.handlers.serverName, version: this.handlers.serverVersion },
|
|
135
|
+
instructions: this.handlers.instructions,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async callTool(params) {
|
|
139
|
+
const name = params.name;
|
|
140
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
141
|
+
throw new McpRpcError(MCP_ERROR_CODES.invalidParams, 'tools/call requires a tool name');
|
|
142
|
+
}
|
|
143
|
+
const rawArguments = params.arguments;
|
|
144
|
+
if (rawArguments !== undefined && !isRecord(rawArguments)) {
|
|
145
|
+
throw new McpRpcError(MCP_ERROR_CODES.invalidParams, 'tools/call arguments must be an object');
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
return await this.handlers.callTool(this.resolveSession(params), name, rawArguments ?? {});
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
// A failing DSH tool is an expected outcome, not a protocol error: the
|
|
152
|
+
// model has to see the failure text to decide what to do next.
|
|
153
|
+
return { content: [{ type: 'text', text: errorMessage(error) }], isError: true };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Resolve which AGY conversation a request belongs to. The transport-level
|
|
158
|
+
* handle (the secret path segment) wins, because it is what the endpoint
|
|
159
|
+
* minted for this bridge; `_meta.sessionId` is the client's own hint and is
|
|
160
|
+
* used only when the transport supplies nothing.
|
|
161
|
+
*/
|
|
162
|
+
resolveSession(params) {
|
|
163
|
+
if (this.sessionHandle.length > 0)
|
|
164
|
+
return this.sessionHandle;
|
|
165
|
+
return sessionOf(params);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const NOTIFICATION = Symbol('mcp-notification');
|
|
169
|
+
export class McpRpcError extends Error {
|
|
170
|
+
code;
|
|
171
|
+
data;
|
|
172
|
+
constructor(code, message, data) {
|
|
173
|
+
super(message);
|
|
174
|
+
this.name = 'McpRpcError';
|
|
175
|
+
this.code = code;
|
|
176
|
+
this.data = data;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* AGY passes its own session handle as `_meta.sessionId` on every request; the
|
|
181
|
+
* HTTP route also carries it in the path. Both are read here so one server can
|
|
182
|
+
* serve several concurrent AGY conversations.
|
|
183
|
+
*/
|
|
184
|
+
function sessionOf(params) {
|
|
185
|
+
const meta = params._meta;
|
|
186
|
+
if (isRecord(meta)) {
|
|
187
|
+
const sessionId = meta.sessionId ?? meta.session_id;
|
|
188
|
+
if (typeof sessionId === 'string' && sessionId.length > 0)
|
|
189
|
+
return sessionId;
|
|
190
|
+
}
|
|
191
|
+
return '';
|
|
192
|
+
}
|
|
193
|
+
/** Serialize one response as a single newline-terminated JSON-RPC frame. */
|
|
194
|
+
export function encodeMcpFrame(response) {
|
|
195
|
+
return `${JSON.stringify(response)}\n`;
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { McpToolDescriptor } from './protocol.js';
|
|
2
|
+
/** The subset of `ctx.tools` this module needs. */
|
|
3
|
+
export interface DshToolRegistryLike {
|
|
4
|
+
schemas(scope?: unknown): readonly unknown[];
|
|
5
|
+
}
|
|
6
|
+
export interface DshToolSchemaLike {
|
|
7
|
+
readonly name?: unknown;
|
|
8
|
+
readonly description?: unknown;
|
|
9
|
+
readonly parameters?: unknown;
|
|
10
|
+
}
|
|
11
|
+
export interface ToolProjectionOptions {
|
|
12
|
+
/** Restrict the projection to these tool names (empty/absent = every tool). */
|
|
13
|
+
readonly allowlist?: readonly string[];
|
|
14
|
+
/** Upper bound on projected tools; guards a pathological registry. */
|
|
15
|
+
readonly maxTools?: number;
|
|
16
|
+
/**
|
|
17
|
+
* The viewing scope, i.e. the live agent of the DSH session the bridge serves.
|
|
18
|
+
* This is load-bearing: `ctx.tools.schemas()` with no scope answers with the
|
|
19
|
+
* GLOBAL view, and `read`/`write`/`grep`/`pwsh` are no longer in the global
|
|
20
|
+
* layer — the official presets moved them onto the agent plane, so they reach
|
|
21
|
+
* a scope only as ancestor contributions. A scope-less projection therefore
|
|
22
|
+
* hands AGY the UI/memory tools and silently drops every file and shell tool.
|
|
23
|
+
*/
|
|
24
|
+
readonly scope?: unknown;
|
|
25
|
+
}
|
|
26
|
+
export interface ToolProjectionResult {
|
|
27
|
+
readonly tools: readonly McpToolDescriptor[];
|
|
28
|
+
/**
|
|
29
|
+
* Names dropped by the allowlist, the built-in exclusions, or the cap. The
|
|
30
|
+
* caller cannot tell those three apart from this list alone, which is why the
|
|
31
|
+
* bridge filters the built-ins out before it reports what the model is
|
|
32
|
+
* missing: a cap overflow has to be visible in the log, and for two rounds it
|
|
33
|
+
* was not.
|
|
34
|
+
*/
|
|
35
|
+
readonly excluded: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Tools that must never be projected to AGY. These are either DSH-internal
|
|
39
|
+
* controls that would let AGY steer its own host, or tools whose execution
|
|
40
|
+
* requires a DSH agent identity that an MCP-originated call does not carry.
|
|
41
|
+
*
|
|
42
|
+
* Exported so the bridge can tell "dropped on purpose" apart from "dropped by
|
|
43
|
+
* the cap" when it reports what the model is missing.
|
|
44
|
+
*/
|
|
45
|
+
export declare const NEVER_PROJECTED_TOOLS: ReadonlySet<string>;
|
|
46
|
+
/**
|
|
47
|
+
* Project the registry. `listChanged: false` is advertised to AGY, so the set
|
|
48
|
+
* is snapshotted per request rather than pushed; a profile change simply takes
|
|
49
|
+
* effect on the next turn.
|
|
50
|
+
*/
|
|
51
|
+
/**
|
|
52
|
+
* Check a call's arguments against the projected schema, mechanically.
|
|
53
|
+
*
|
|
54
|
+
* AGY does not honour `required` and sometimes sends the wrong primitive
|
|
55
|
+
* (`{"query":123}` for a string field, `{}` for a call that needs one). Left
|
|
56
|
+
* unchecked, every such call still walks the host guard chain and the approval
|
|
57
|
+
* service before the schema layer rejects it — so the bridge answers first, with
|
|
58
|
+
* the same wording the host uses. Anything this function cannot decide
|
|
59
|
+
* (unknown property, no declared type) is left to DSH: a bridge that guesses
|
|
60
|
+
* would be a second, weaker validator pretending to be the real one.
|
|
61
|
+
*/
|
|
62
|
+
export declare function validateToolArguments(schema: unknown, args: unknown): string[];
|
|
63
|
+
export declare function projectDshTools(registry: DshToolRegistryLike | undefined, options?: ToolProjectionOptions): ToolProjectionResult;
|
|
64
|
+
//# sourceMappingURL=tool-projection.d.ts.map
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project the live DSH tool registry into MCP tool descriptors.
|
|
3
|
+
*
|
|
4
|
+
* This is the replacement for the old DSH-owned prompt contract: instead of
|
|
5
|
+
* serializing every tool schema into the prompt (58 KB on a stock profile with
|
|
6
|
+
* 18 bundles, which the 56 KiB prompt budget could not hold), the same schemas
|
|
7
|
+
* travel over MCP and cost zero prompt bytes.
|
|
8
|
+
*
|
|
9
|
+
* The schemas are sanitized by `sanitizeSchema` from the structured tool
|
|
10
|
+
* protocol, so MCP exposure and the legacy envelope agree on exactly which
|
|
11
|
+
* JSON Schema keywords are allowed to leave the host.
|
|
12
|
+
*/
|
|
13
|
+
import { sanitizeSchema, TOOL_PROTOCOL_LIMITS } from '../provider/tool-protocol.js';
|
|
14
|
+
/**
|
|
15
|
+
* Tools that must never be projected to AGY. These are either DSH-internal
|
|
16
|
+
* controls that would let AGY steer its own host, or tools whose execution
|
|
17
|
+
* requires a DSH agent identity that an MCP-originated call does not carry.
|
|
18
|
+
*
|
|
19
|
+
* Exported so the bridge can tell "dropped on purpose" apart from "dropped by
|
|
20
|
+
* the cap" when it reports what the model is missing.
|
|
21
|
+
*/
|
|
22
|
+
export const NEVER_PROJECTED_TOOLS = new Set([
|
|
23
|
+
'compress',
|
|
24
|
+
'decompress',
|
|
25
|
+
'search_context',
|
|
26
|
+
'acp_status',
|
|
27
|
+
'acp_cache',
|
|
28
|
+
]);
|
|
29
|
+
const FALLBACK_SCHEMA = Object.freeze({
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {},
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
});
|
|
34
|
+
function isRecord(value) {
|
|
35
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
function readSchemaName(value) {
|
|
38
|
+
if (!isRecord(value))
|
|
39
|
+
return undefined;
|
|
40
|
+
const name = value.name;
|
|
41
|
+
return typeof name === 'string' && name.length > 0 ? name : undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Project the registry. `listChanged: false` is advertised to AGY, so the set
|
|
45
|
+
* is snapshotted per request rather than pushed; a profile change simply takes
|
|
46
|
+
* effect on the next turn.
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* Check a call's arguments against the projected schema, mechanically.
|
|
50
|
+
*
|
|
51
|
+
* AGY does not honour `required` and sometimes sends the wrong primitive
|
|
52
|
+
* (`{"query":123}` for a string field, `{}` for a call that needs one). Left
|
|
53
|
+
* unchecked, every such call still walks the host guard chain and the approval
|
|
54
|
+
* service before the schema layer rejects it — so the bridge answers first, with
|
|
55
|
+
* the same wording the host uses. Anything this function cannot decide
|
|
56
|
+
* (unknown property, no declared type) is left to DSH: a bridge that guesses
|
|
57
|
+
* would be a second, weaker validator pretending to be the real one.
|
|
58
|
+
*/
|
|
59
|
+
export function validateToolArguments(schema, args) {
|
|
60
|
+
if (!isRecord(schema) || schema.type !== 'object')
|
|
61
|
+
return [];
|
|
62
|
+
const problems = [];
|
|
63
|
+
const given = isRecord(args) ? args : undefined;
|
|
64
|
+
if (given === undefined) {
|
|
65
|
+
return [`arguments must be an object, got ${Array.isArray(args) ? 'array' : typeof args}`];
|
|
66
|
+
}
|
|
67
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
68
|
+
for (const key of required) {
|
|
69
|
+
if (typeof key !== 'string')
|
|
70
|
+
continue;
|
|
71
|
+
if (given[key] === undefined)
|
|
72
|
+
problems.push(`missing required property "${key}"`);
|
|
73
|
+
}
|
|
74
|
+
const properties = isRecord(schema.properties) ? schema.properties : undefined;
|
|
75
|
+
if (properties !== undefined) {
|
|
76
|
+
for (const [key, value] of Object.entries(given)) {
|
|
77
|
+
const declared = properties[key];
|
|
78
|
+
if (!isRecord(declared) || value === undefined || value === null)
|
|
79
|
+
continue;
|
|
80
|
+
const expected = declared.type;
|
|
81
|
+
if (typeof expected !== 'string')
|
|
82
|
+
continue;
|
|
83
|
+
if (!matchesJsonType(value, expected)) {
|
|
84
|
+
problems.push(`"${key}" must be ${expectedDescriptionOf(expected)}, got ${typeof value}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return problems;
|
|
89
|
+
}
|
|
90
|
+
function matchesJsonType(value, expected) {
|
|
91
|
+
switch (expected) {
|
|
92
|
+
case 'string': return typeof value === 'string';
|
|
93
|
+
case 'number': return typeof value === 'number';
|
|
94
|
+
case 'integer': return typeof value === 'number' && Number.isInteger(value);
|
|
95
|
+
case 'boolean': return typeof value === 'boolean';
|
|
96
|
+
case 'array': return Array.isArray(value);
|
|
97
|
+
case 'object': return isRecord(value);
|
|
98
|
+
default: return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function expectedDescriptionOf(expected) {
|
|
102
|
+
switch (expected) {
|
|
103
|
+
case 'integer': return 'an integer';
|
|
104
|
+
case 'array': return 'an array';
|
|
105
|
+
default: return `a ${expected}`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function projectDshTools(registry, options = {}) {
|
|
109
|
+
const excluded = [];
|
|
110
|
+
const maxTools = options.maxTools ?? TOOL_PROTOCOL_LIMITS.maxTools;
|
|
111
|
+
if (registry === undefined || typeof registry.schemas !== 'function') {
|
|
112
|
+
return { tools: [], excluded };
|
|
113
|
+
}
|
|
114
|
+
let raw;
|
|
115
|
+
try {
|
|
116
|
+
raw = registry.schemas(options.scope) ?? [];
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return { tools: [], excluded };
|
|
120
|
+
}
|
|
121
|
+
const allow = options.allowlist === undefined || options.allowlist.length === 0
|
|
122
|
+
? undefined
|
|
123
|
+
: new Set(options.allowlist);
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
const tools = [];
|
|
126
|
+
for (const entry of raw) {
|
|
127
|
+
const name = readSchemaName(entry);
|
|
128
|
+
if (name === undefined)
|
|
129
|
+
continue;
|
|
130
|
+
if (seen.has(name))
|
|
131
|
+
continue;
|
|
132
|
+
seen.add(name);
|
|
133
|
+
if (NEVER_PROJECTED_TOOLS.has(name)) {
|
|
134
|
+
excluded.push(name);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (allow !== undefined && !allow.has(name)) {
|
|
138
|
+
excluded.push(name);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (tools.length >= maxTools) {
|
|
142
|
+
excluded.push(name);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const source = entry;
|
|
146
|
+
let parameters;
|
|
147
|
+
try {
|
|
148
|
+
const sanitized = sanitizeSchema(source.parameters, 0, `tool ${name}.parameters`);
|
|
149
|
+
parameters = isRecord(sanitized) && sanitized.type === 'object' ? sanitized : { ...FALLBACK_SCHEMA };
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// An unsupported keyword must not remove a working tool from the model's
|
|
153
|
+
// view; fall back to an open object and let DSH validate the arguments.
|
|
154
|
+
parameters = { ...FALLBACK_SCHEMA };
|
|
155
|
+
}
|
|
156
|
+
const description = typeof source.description === 'string' ? source.description : '';
|
|
157
|
+
tools.push({
|
|
158
|
+
name,
|
|
159
|
+
description: description.length > TOOL_PROTOCOL_LIMITS.maxDescriptionLength
|
|
160
|
+
? description.slice(0, TOOL_PROTOCOL_LIMITS.maxDescriptionLength)
|
|
161
|
+
: description,
|
|
162
|
+
inputSchema: parameters,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
tools.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
166
|
+
return { tools, excluded };
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=tool-projection.js.map
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
2
|
+
import { LlmAdapter } from '@deepseek-ai/dsh-llm';
|
|
3
|
+
import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, StreamChunk, TokenUsage, ResolvedRetryPolicy } from '@deepseek-ai/dsh-llm';
|
|
4
|
+
import { type PersistentTransportResult } from '../agy/persistent-transport.js';
|
|
5
|
+
import { type AgyReasoningEffort, type AgyRequest, type ProcessResult } from '../agy/process.js';
|
|
6
|
+
import { type AgyModelDiscoveryCommand, type AgyModelDiscoveryResult } from '../agy/models.js';
|
|
7
|
+
import { type AgyDiagnosticResult } from '../agy/diagnostics.js';
|
|
8
|
+
import { AgyConcurrencyLimiter } from '../agy/limiter.js';
|
|
9
|
+
import { type AgyLogger } from '../agy/log.js';
|
|
10
|
+
import { type Config, type ModelConfig } from './config.js';
|
|
11
|
+
import { SessionRegistry, type SessionRecord } from '../session/store.js';
|
|
12
|
+
import { type DshContextLookup } from '../dsh/context.js';
|
|
13
|
+
export type AgyProcessRunner = (request: AgyRequest) => Promise<ProcessResult>;
|
|
14
|
+
export interface AgyAdapterDependencies {
|
|
15
|
+
/** Injectable seam for deterministic tests; production uses `runAgyProcess`. */
|
|
16
|
+
runAgyProcess?: AgyProcessRunner;
|
|
17
|
+
/** Injectable quota-free `agy models` command for deterministic tests. */
|
|
18
|
+
runModelDiscovery?: AgyModelDiscoveryCommand;
|
|
19
|
+
/** Injectable session mapping; production uses the durable file-backed store. */
|
|
20
|
+
sessionRegistry?: SessionRegistry;
|
|
21
|
+
/** Optional structured logger; logging failures never affect the request. */
|
|
22
|
+
logger?: AgyLogger;
|
|
23
|
+
/** Injectable process limiter for deterministic concurrency tests. */
|
|
24
|
+
concurrencyLimiter?: AgyConcurrencyLimiter;
|
|
25
|
+
/** Optional DSH AttachmentStore; text-only requests do not require it. */
|
|
26
|
+
attachmentStore?: Pick<AttachmentStore, 'readImage'>;
|
|
27
|
+
/** Lazy resolver for runtimes that compose the optional store after this plugin. */
|
|
28
|
+
resolveAttachmentStore?: () => Pick<AttachmentStore, 'readImage'> | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Injectable seam for deterministic tests of the persistent path; production
|
|
31
|
+
* leaves it undefined and the transport is built from the resolved AGY
|
|
32
|
+
* executable.
|
|
33
|
+
*/
|
|
34
|
+
persistentTransport?: PersistentTransportLike;
|
|
35
|
+
/** Optional Cordis reflection seam for the DSH-owned capability context. */
|
|
36
|
+
dshContext?: DshContextLookup;
|
|
37
|
+
/**
|
|
38
|
+
* Resolves the live MCP tool bridge, if the composition has one. Lazy
|
|
39
|
+
* because the bridge is mounted on the host webServer and its fiber may
|
|
40
|
+
* start after this adapter is constructed.
|
|
41
|
+
*/
|
|
42
|
+
resolveToolBridge?: () => DshToolBridgeHandle | undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The slice of the persistent transport the adapter needs. Declared
|
|
46
|
+
* structurally so tests can drive the streaming path without a real AGY
|
|
47
|
+
* worker process.
|
|
48
|
+
*/
|
|
49
|
+
export interface PersistentTransportLike {
|
|
50
|
+
request(input: {
|
|
51
|
+
readonly sessionId: string;
|
|
52
|
+
readonly text: string;
|
|
53
|
+
readonly signal?: AbortSignal;
|
|
54
|
+
readonly timeoutMs?: number;
|
|
55
|
+
readonly idleTimeoutMs?: number;
|
|
56
|
+
readonly maxOutputBytes?: number;
|
|
57
|
+
readonly onEvent?: (payload: unknown) => void;
|
|
58
|
+
readonly agent?: string;
|
|
59
|
+
readonly model?: string;
|
|
60
|
+
readonly reasoningEffort?: AgyReasoningEffort;
|
|
61
|
+
readonly conversation?: string;
|
|
62
|
+
readonly extraArgs?: readonly string[];
|
|
63
|
+
}): Promise<PersistentTransportResult>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The slice of the MCP tool bridge the adapter needs. Declared structurally so
|
|
67
|
+
* the provider never imports the bridge module's host-service types.
|
|
68
|
+
*/
|
|
69
|
+
export interface DshToolBridgeHandle {
|
|
70
|
+
readonly active: boolean;
|
|
71
|
+
readonly inactiveReason: string | undefined;
|
|
72
|
+
/** Point the bridge at the DSH session owning the next AGY turn. */
|
|
73
|
+
readonly setSession: (sessionId: string) => void;
|
|
74
|
+
/** URL AGY reaches the bridge on; undefined until the bridge is live. */
|
|
75
|
+
readonly url: string | undefined;
|
|
76
|
+
}
|
|
77
|
+
/** Convert AGY's observed snake/camel-case usage variants to DSH accounting. */
|
|
78
|
+
export declare function mapAgyUsage(raw: Record<string, unknown>): TokenUsage;
|
|
79
|
+
export declare const __effortOverrideCacheForTest: {
|
|
80
|
+
at: number;
|
|
81
|
+
value: AgyReasoningEffort | undefined;
|
|
82
|
+
};
|
|
83
|
+
/** DSH text-only adapter backed by the locally authenticated AGY CLI. */
|
|
84
|
+
export declare class AgyAdapter extends LlmAdapter {
|
|
85
|
+
private readonly model;
|
|
86
|
+
private readonly models;
|
|
87
|
+
private readonly agent;
|
|
88
|
+
private readonly workspaceRoot;
|
|
89
|
+
private readonly addDirs;
|
|
90
|
+
private readonly mode;
|
|
91
|
+
private readonly disableSlashCommands;
|
|
92
|
+
private readonly toolPolicy;
|
|
93
|
+
private readonly dshContext;
|
|
94
|
+
private readonly agyPath;
|
|
95
|
+
private readonly timeoutMs;
|
|
96
|
+
private readonly idleTimeoutMs;
|
|
97
|
+
private readonly stdinMode;
|
|
98
|
+
private readonly run;
|
|
99
|
+
private readonly sessionMode;
|
|
100
|
+
private readonly sessions;
|
|
101
|
+
private readonly minimumAgyVersion;
|
|
102
|
+
private readonly limiter;
|
|
103
|
+
private readonly logger;
|
|
104
|
+
private readonly maxOutputBytes;
|
|
105
|
+
private readonly maxEventLineLength;
|
|
106
|
+
private readonly inputFrameLimitBytes;
|
|
107
|
+
private readonly maxSingleToolResultBytes;
|
|
108
|
+
private readonly maxHistoricalToolResultBytes;
|
|
109
|
+
private readonly toolProtocolRepairRetries;
|
|
110
|
+
private readonly toolProtocolPlainTextFallback;
|
|
111
|
+
private readonly retryPolicy;
|
|
112
|
+
private readonly purposeRoutes;
|
|
113
|
+
private readonly transport;
|
|
114
|
+
private readonly persistentIdleTtlMs;
|
|
115
|
+
private readonly persistentReadyTimeoutMs;
|
|
116
|
+
private readonly persistentFallback;
|
|
117
|
+
private readonly persistentTransport;
|
|
118
|
+
private readonly imageInput;
|
|
119
|
+
private readonly imageAgent;
|
|
120
|
+
private readonly resolveAttachmentStore;
|
|
121
|
+
private readonly resolveToolBridge;
|
|
122
|
+
private readonly mcpBridgeEnabled;
|
|
123
|
+
private readonly discovery;
|
|
124
|
+
private readonly visibleModels;
|
|
125
|
+
private currentModels;
|
|
126
|
+
private modelDiscoveryResult;
|
|
127
|
+
constructor(config?: Config, dependencies?: AgyAdapterDependencies);
|
|
128
|
+
private preparePrompt;
|
|
129
|
+
/** Remove the in-memory AGY mapping; the next call sends complete DSH history. */
|
|
130
|
+
clearSession(sessionId: string): boolean;
|
|
131
|
+
/** Inspect the detached mapping for diagnostics and tests. */
|
|
132
|
+
getSession(sessionId: string): SessionRecord | undefined;
|
|
133
|
+
/** Return process-slot state without exposing prompts, paths, or credentials. */
|
|
134
|
+
getConcurrencyStats(): import("../agy/limiter.js").AgyConcurrencyStats;
|
|
135
|
+
/** Check AGY path, version, and configured Agent without spending model quota. */
|
|
136
|
+
diagnose(): Promise<AgyDiagnosticResult>;
|
|
137
|
+
providerInfo(provider: string): LlmProviderInfo;
|
|
138
|
+
providerRetryPolicy(_provider: string): ResolvedRetryPolicy;
|
|
139
|
+
listModels(provider: string): Promise<readonly LlmModelInfo[]>;
|
|
140
|
+
/** Expose safe discovery state for diagnostics and integration tests. */
|
|
141
|
+
getModelDiscoveryStatus(): AgyModelDiscoveryResult | {
|
|
142
|
+
source: 'static';
|
|
143
|
+
stale: false;
|
|
144
|
+
models: readonly ModelConfig[];
|
|
145
|
+
};
|
|
146
|
+
resolveModel(provider: string, model: string, _signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
|
|
147
|
+
private shouldUsePersistent;
|
|
148
|
+
private streamPersistentAttempt;
|
|
149
|
+
private effectiveModels;
|
|
150
|
+
stream(options: GenerateOptions): AsyncGenerator<StreamChunk, void, void>;
|
|
151
|
+
private streamAttempt;
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=agy.d.ts.map
|