@zleap-ai/dsh-sag 0.1.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/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +58 -0
- package/README.zh.md +58 -0
- package/THIRD_PARTY_NOTICES +671 -0
- package/cordis.patch.yml +5 -0
- package/docs/embedded.md +61 -0
- package/lib/brand.d.ts +13 -0
- package/lib/brand.js +15 -0
- package/lib/cli/runtime.d.ts +40 -0
- package/lib/cli/runtime.js +121 -0
- package/lib/cli.d.ts +21 -0
- package/lib/cli.js +265 -0
- package/lib/config.d.ts +60 -0
- package/lib/config.js +120 -0
- package/lib/connection/descriptor.d.ts +4 -0
- package/lib/connection/descriptor.js +66 -0
- package/lib/connection/discovery.d.ts +40 -0
- package/lib/connection/discovery.js +139 -0
- package/lib/connection/guidance.d.ts +7 -0
- package/lib/connection/guidance.js +7 -0
- package/lib/connection/manager.d.ts +67 -0
- package/lib/connection/manager.js +273 -0
- package/lib/connection/store.d.ts +42 -0
- package/lib/connection/store.js +164 -0
- package/lib/connection/types.d.ts +35 -0
- package/lib/connection/types.js +2 -0
- package/lib/dsh-sag-cli.js +32202 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.js +68 -0
- package/lib/local/api-client.d.ts +157 -0
- package/lib/local/api-client.js +338 -0
- package/lib/local/gateway.d.ts +43 -0
- package/lib/local/gateway.js +34 -0
- package/lib/local/mcp-probe.d.ts +27 -0
- package/lib/local/mcp-probe.js +92 -0
- package/lib/presentation.d.ts +8 -0
- package/lib/presentation.js +7 -0
- package/lib/runtime/client.d.ts +22 -0
- package/lib/runtime/client.js +117 -0
- package/lib/runtime/protocol.d.ts +113 -0
- package/lib/runtime/protocol.js +118 -0
- package/lib/runtime/supervisor.d.ts +30 -0
- package/lib/runtime/supervisor.js +107 -0
- package/lib/tools/documents.d.ts +8 -0
- package/lib/tools/documents.js +134 -0
- package/lib/tools/ingest.d.ts +5 -0
- package/lib/tools/ingest.js +35 -0
- package/lib/tools/local.d.ts +40 -0
- package/lib/tools/local.js +111 -0
- package/lib/tools/output.d.ts +96 -0
- package/lib/tools/output.js +67 -0
- package/lib/tools/read.d.ts +6 -0
- package/lib/tools/read.js +84 -0
- package/lib/tools/search.d.ts +6 -0
- package/lib/tools/search.js +107 -0
- package/lib/tools/sources.d.ts +5 -0
- package/lib/tools/sources.js +61 -0
- package/lib/tools/status.d.ts +5 -0
- package/lib/tools/status.js +28 -0
- package/lib/tools/upload.d.ts +6 -0
- package/lib/tools/upload.js +68 -0
- package/package.json +96 -0
- package/runtime/pyproject.toml +29 -0
- package/runtime/src/dsh_sag_runtime/__init__.py +3 -0
- package/runtime/src/dsh_sag_runtime/__main__.py +80 -0
- package/runtime/src/dsh_sag_runtime/engines.py +139 -0
- package/runtime/src/dsh_sag_runtime/errors.py +47 -0
- package/runtime/src/dsh_sag_runtime/evidence.py +53 -0
- package/runtime/src/dsh_sag_runtime/protocol.py +161 -0
- package/runtime/src/dsh_sag_runtime/read.py +61 -0
- package/runtime/src/dsh_sag_runtime/search.py +76 -0
- package/runtime/src/dsh_sag_runtime/server.py +136 -0
- package/runtime/uv.lock +2310 -0
- package/scripts/setup-runtime.mjs +47 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SagConnectionDescriptor } from '../connection/types.js';
|
|
2
|
+
/** Successful MCP compatibility result; no MCP tools are registered in dsh. */
|
|
3
|
+
export interface McpProbeResult {
|
|
4
|
+
readonly tools: readonly string[];
|
|
5
|
+
readonly readTool?: 'read' | 'get_chunk';
|
|
6
|
+
}
|
|
7
|
+
/** One temporary MCP connection, injectable so lifecycle behavior is observable. */
|
|
8
|
+
export interface McpProbeSession {
|
|
9
|
+
connect(signal: AbortSignal): Promise<void>;
|
|
10
|
+
listTools(signal: AbortSignal): Promise<readonly string[]>;
|
|
11
|
+
close(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
/** Factory for a temporary MCP probe session. */
|
|
14
|
+
export type McpProbeSessionFactory = (descriptor: SagConnectionDescriptor) => McpProbeSession;
|
|
15
|
+
/** A reachable MCP endpoint whose advertised tools do not satisfy dsh-sag. */
|
|
16
|
+
export declare class McpProbeIncompatibleError extends Error {
|
|
17
|
+
/** @param message - redacted compatibility failure. */
|
|
18
|
+
constructor(message: string);
|
|
19
|
+
}
|
|
20
|
+
/** A temporary transport, initialization, listing, or closure failure. */
|
|
21
|
+
export declare class McpProbeUnreachableError extends Error {
|
|
22
|
+
/** @param message - redacted connection failure. */
|
|
23
|
+
constructor(message: string);
|
|
24
|
+
}
|
|
25
|
+
/** Initialize SAG MCP, verify retrieval tools, and require successful temporary-session closure. */
|
|
26
|
+
export declare function probeMcp(descriptor: SagConnectionDescriptor, signal: AbortSignal, factory?: McpProbeSessionFactory, advertisedCapabilities?: readonly string[]): Promise<McpProbeResult>;
|
|
27
|
+
//# sourceMappingURL=mcp-probe.d.ts.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
3
|
+
/** A reachable MCP endpoint whose advertised tools do not satisfy dsh-sag. */
|
|
4
|
+
export class McpProbeIncompatibleError extends Error {
|
|
5
|
+
/** @param message - redacted compatibility failure. */
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(`dsh-sag: SAG MCP is incompatible: ${message}`);
|
|
8
|
+
this.name = 'McpProbeIncompatibleError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** A temporary transport, initialization, listing, or closure failure. */
|
|
12
|
+
export class McpProbeUnreachableError extends Error {
|
|
13
|
+
/** @param message - redacted connection failure. */
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(`dsh-sag: SAG MCP is unreachable: ${message}`);
|
|
16
|
+
this.name = 'McpProbeUnreachableError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function redacted(error, token) {
|
|
20
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
21
|
+
return message.split(token).join('<redacted>');
|
|
22
|
+
}
|
|
23
|
+
function productionSession(descriptor) {
|
|
24
|
+
const client = new Client({ name: 'dsh-sag-probe', version: '1.0.0' });
|
|
25
|
+
const transport = new StreamableHTTPClientTransport(new URL(descriptor.mcpUrl), {
|
|
26
|
+
requestInit: { headers: { Authorization: `Bearer ${descriptor.accessToken}` } },
|
|
27
|
+
});
|
|
28
|
+
let connected = false;
|
|
29
|
+
return {
|
|
30
|
+
async connect(signal) {
|
|
31
|
+
await client.connect(transport, { signal });
|
|
32
|
+
connected = true;
|
|
33
|
+
},
|
|
34
|
+
async listTools(signal) {
|
|
35
|
+
const response = await client.listTools({}, { signal });
|
|
36
|
+
return response.tools.map(tool => tool.name);
|
|
37
|
+
},
|
|
38
|
+
async close() {
|
|
39
|
+
if (connected)
|
|
40
|
+
await client.close();
|
|
41
|
+
else
|
|
42
|
+
await transport.close();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function compatible(names, advertisedCapabilities) {
|
|
47
|
+
const tools = [...names].sort();
|
|
48
|
+
const required = advertisedCapabilities === undefined ? ['list_sources', 'search'] : [
|
|
49
|
+
...(advertisedCapabilities.includes('knowledge.search') ? ['search'] : []),
|
|
50
|
+
];
|
|
51
|
+
for (const tool of required)
|
|
52
|
+
if (!tools.includes(tool))
|
|
53
|
+
throw new McpProbeIncompatibleError(`required tool ${tool} is missing`);
|
|
54
|
+
const readTool = tools.includes('read') ? 'read' : tools.includes('get_chunk') ? 'get_chunk' : undefined;
|
|
55
|
+
if ((advertisedCapabilities === undefined || advertisedCapabilities.includes('knowledge.read')) && readTool === undefined) {
|
|
56
|
+
throw new McpProbeIncompatibleError('required tool read or get_chunk is missing');
|
|
57
|
+
}
|
|
58
|
+
return { tools, ...(readTool === undefined ? {} : { readTool }) };
|
|
59
|
+
}
|
|
60
|
+
/** Initialize SAG MCP, verify retrieval tools, and require successful temporary-session closure. */
|
|
61
|
+
export async function probeMcp(descriptor, signal, factory = productionSession, advertisedCapabilities) {
|
|
62
|
+
if (signal.aborted)
|
|
63
|
+
throw signal.reason;
|
|
64
|
+
const session = factory(descriptor);
|
|
65
|
+
let result;
|
|
66
|
+
let failure;
|
|
67
|
+
try {
|
|
68
|
+
await session.connect(signal);
|
|
69
|
+
result = compatible(await session.listTools(signal), advertisedCapabilities);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
failure = error instanceof McpProbeIncompatibleError
|
|
73
|
+
? error
|
|
74
|
+
: new McpProbeUnreachableError(redacted(error, descriptor.accessToken));
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
await session.close();
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
const closeFailure = new McpProbeUnreachableError(`close failed: ${redacted(error, descriptor.accessToken)}`);
|
|
81
|
+
if (failure === undefined)
|
|
82
|
+
failure = closeFailure;
|
|
83
|
+
}
|
|
84
|
+
if (signal.aborted)
|
|
85
|
+
throw signal.reason;
|
|
86
|
+
if (failure !== undefined)
|
|
87
|
+
throw failure;
|
|
88
|
+
if (result === undefined)
|
|
89
|
+
throw new McpProbeUnreachableError('probe completed without a result');
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=mcp-probe.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ToolCallView } from '@deepseek-ai/dsh-tools/presentation';
|
|
2
|
+
export declare function presentSearchCall(args: {
|
|
3
|
+
readonly query: string;
|
|
4
|
+
}): ToolCallView;
|
|
5
|
+
export declare function presentReadCall(args: {
|
|
6
|
+
readonly evidence_ref: string;
|
|
7
|
+
}): ToolCallView;
|
|
8
|
+
//# sourceMappingURL=presentation.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function presentSearchCall(args) {
|
|
2
|
+
return { card: 'generic', title: 'Search SAG knowledge', kind: 'search', rawInput: args.query };
|
|
3
|
+
}
|
|
4
|
+
export function presentReadCall(args) {
|
|
5
|
+
return { card: 'generic', title: 'Read SAG evidence', kind: 'read', rawInput: args.evidence_ref };
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=presentation.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Readable, Writable } from 'node:stream';
|
|
2
|
+
import { type RpcResult } from './protocol.js';
|
|
3
|
+
/** Structured rejection returned by the Python runtime. */
|
|
4
|
+
export declare class SagRuntimeError extends Error {
|
|
5
|
+
readonly name = "SagRuntimeError";
|
|
6
|
+
readonly code: string;
|
|
7
|
+
readonly data: Readonly<Record<string, unknown>>;
|
|
8
|
+
constructor(message: string, code: string, data: Readonly<Record<string, unknown>>);
|
|
9
|
+
}
|
|
10
|
+
/** Correlated JSON-RPC client over one managed sidecar's stdio streams. */
|
|
11
|
+
export declare class SagRuntimeClient {
|
|
12
|
+
#private;
|
|
13
|
+
constructor(stdin: Writable, stdout: Readable, done: Promise<{
|
|
14
|
+
readonly exitCode: number | null;
|
|
15
|
+
readonly signal: NodeJS.Signals | null;
|
|
16
|
+
}>);
|
|
17
|
+
/** Send one request; cancellation remains live until the sidecar settles it. */
|
|
18
|
+
request(method: 'initialize' | 'search' | 'read' | 'shutdown', params: Readonly<Record<string, unknown>>, signal?: AbortSignal): Promise<RpcResult>;
|
|
19
|
+
/** Stop admission and close the protocol input. */
|
|
20
|
+
closeInput(): void;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { SagProtocolError, decodeResponse, encodeRequest, } from './protocol.js';
|
|
2
|
+
/** Structured rejection returned by the Python runtime. */
|
|
3
|
+
export class SagRuntimeError extends Error {
|
|
4
|
+
name = 'SagRuntimeError';
|
|
5
|
+
code;
|
|
6
|
+
data;
|
|
7
|
+
constructor(message, code, data) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.data = data;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Correlated JSON-RPC client over one managed sidecar's stdio streams. */
|
|
14
|
+
export class SagRuntimeClient {
|
|
15
|
+
#stdin;
|
|
16
|
+
#stdout;
|
|
17
|
+
#pending = new Map();
|
|
18
|
+
#decoder = new TextDecoder('utf-8', { fatal: true });
|
|
19
|
+
#buffer = '';
|
|
20
|
+
#nextId = 1;
|
|
21
|
+
#closedError;
|
|
22
|
+
constructor(stdin, stdout, done) {
|
|
23
|
+
this.#stdin = stdin;
|
|
24
|
+
this.#stdout = stdout;
|
|
25
|
+
stdout.on('data', (chunk) => this.#consume(chunk));
|
|
26
|
+
stdout.on('error', error => this.#failProtocol(error));
|
|
27
|
+
stdin.on('error', error => this.#failProtocol(error));
|
|
28
|
+
void done.then(outcome => this.#close(new Error(`SAG runtime exited (code=${outcome.exitCode ?? 'null'}, signal=${outcome.signal ?? 'null'})`)), error => this.#close(error instanceof Error ? error : new Error('SAG runtime spawn failed')));
|
|
29
|
+
}
|
|
30
|
+
/** Send one request; cancellation remains live until the sidecar settles it. */
|
|
31
|
+
request(method, params, signal) {
|
|
32
|
+
if (this.#closedError)
|
|
33
|
+
return Promise.reject(new Error(`SAG runtime client is closed: ${this.#closedError.message}`));
|
|
34
|
+
if (this.#nextId > Number.MAX_SAFE_INTEGER) {
|
|
35
|
+
this.#close(new Error('SAG runtime request id space exhausted'));
|
|
36
|
+
return Promise.reject(this.#closedError);
|
|
37
|
+
}
|
|
38
|
+
const id = this.#nextId++;
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
let cancelled = false;
|
|
41
|
+
const onAbort = () => {
|
|
42
|
+
if (cancelled || this.#closedError)
|
|
43
|
+
return;
|
|
44
|
+
cancelled = true;
|
|
45
|
+
this.#stdin.write(encodeRequest({ jsonrpc: '2.0', method: '$/cancelRequest', params: { id } }));
|
|
46
|
+
};
|
|
47
|
+
const removeAbort = signal
|
|
48
|
+
? () => signal.removeEventListener('abort', onAbort)
|
|
49
|
+
: undefined;
|
|
50
|
+
this.#pending.set(id, { resolve, reject, ...(removeAbort ? { removeAbort } : {}) });
|
|
51
|
+
if (signal) {
|
|
52
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
53
|
+
}
|
|
54
|
+
const request = { jsonrpc: '2.0', id, method, params };
|
|
55
|
+
this.#stdin.write(encodeRequest(request));
|
|
56
|
+
if (signal?.aborted)
|
|
57
|
+
onAbort();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/** Stop admission and close the protocol input. */
|
|
61
|
+
closeInput() {
|
|
62
|
+
this.#stdin.end();
|
|
63
|
+
}
|
|
64
|
+
#consume(chunk) {
|
|
65
|
+
if (this.#closedError)
|
|
66
|
+
return;
|
|
67
|
+
try {
|
|
68
|
+
this.#buffer += typeof chunk === 'string'
|
|
69
|
+
? chunk
|
|
70
|
+
: this.#decoder.decode(chunk, { stream: true });
|
|
71
|
+
let newline = this.#buffer.indexOf('\n');
|
|
72
|
+
while (newline >= 0) {
|
|
73
|
+
const line = this.#buffer.slice(0, newline).replace(/\r$/, '');
|
|
74
|
+
this.#buffer = this.#buffer.slice(newline + 1);
|
|
75
|
+
if (line)
|
|
76
|
+
this.#acceptLine(line);
|
|
77
|
+
newline = this.#buffer.indexOf('\n');
|
|
78
|
+
}
|
|
79
|
+
if (Buffer.byteLength(this.#buffer, 'utf8') > 8 * 1024 * 1024) {
|
|
80
|
+
throw new SagProtocolError('response frame exceeds 8 MiB');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
this.#failProtocol(error);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
#acceptLine(line) {
|
|
88
|
+
const response = decodeResponse(line);
|
|
89
|
+
const pending = this.#pending.get(response.id);
|
|
90
|
+
if (!pending)
|
|
91
|
+
throw new SagProtocolError(`response id ${response.id} is not live`);
|
|
92
|
+
this.#pending.delete(response.id);
|
|
93
|
+
pending.removeAbort?.();
|
|
94
|
+
if ('error' in response) {
|
|
95
|
+
const data = response.error.data ?? { code: 'RUNTIME_ERROR' };
|
|
96
|
+
pending.reject(new SagRuntimeError(response.error.message, data.code, data));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
pending.resolve(response.result);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
#failProtocol(error) {
|
|
103
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
104
|
+
this.#close(new SagProtocolError(`SAG runtime protocol failure: ${message}`));
|
|
105
|
+
}
|
|
106
|
+
#close(error) {
|
|
107
|
+
if (this.#closedError)
|
|
108
|
+
return;
|
|
109
|
+
this.#closedError = error;
|
|
110
|
+
for (const pending of this.#pending.values()) {
|
|
111
|
+
pending.removeAbort?.();
|
|
112
|
+
pending.reject(error);
|
|
113
|
+
}
|
|
114
|
+
this.#pending.clear();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { SagEvidenceRef, SagNamespaceId } from '../brand.ts';
|
|
2
|
+
export declare const RPC_PROTOCOL_VERSION: "1.0";
|
|
3
|
+
export declare const REQUIRED_ENGINE_VERSION: "0.10.0";
|
|
4
|
+
export declare const MAX_FRAME_BYTES: number;
|
|
5
|
+
export type RpcId = number;
|
|
6
|
+
export type SearchMode = 'fast' | 'precise';
|
|
7
|
+
export interface InitializeResult {
|
|
8
|
+
readonly protocolVersion: string;
|
|
9
|
+
readonly engineVersion: string;
|
|
10
|
+
readonly health: 'available' | 'degraded' | 'unavailable';
|
|
11
|
+
readonly evidenceRead: boolean;
|
|
12
|
+
readonly namespaces: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
export interface SearchEvidence {
|
|
15
|
+
readonly evidenceRef: SagEvidenceRef;
|
|
16
|
+
readonly namespaceId: SagNamespaceId;
|
|
17
|
+
readonly sourceId: string;
|
|
18
|
+
readonly title: string;
|
|
19
|
+
readonly excerpt: string;
|
|
20
|
+
readonly score?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface SearchResult {
|
|
23
|
+
readonly query: string;
|
|
24
|
+
readonly evidences: readonly SearchEvidence[];
|
|
25
|
+
}
|
|
26
|
+
export interface ReadEvent {
|
|
27
|
+
readonly id?: string;
|
|
28
|
+
readonly title?: string;
|
|
29
|
+
readonly summary?: string;
|
|
30
|
+
readonly category?: string;
|
|
31
|
+
readonly rank?: number;
|
|
32
|
+
}
|
|
33
|
+
export interface ReadResult {
|
|
34
|
+
readonly title: string;
|
|
35
|
+
readonly content: string;
|
|
36
|
+
readonly offset: number;
|
|
37
|
+
readonly nextOffset?: number;
|
|
38
|
+
readonly totalChars: number;
|
|
39
|
+
readonly events?: readonly ReadEvent[];
|
|
40
|
+
}
|
|
41
|
+
export interface RpcErrorData {
|
|
42
|
+
readonly code: string;
|
|
43
|
+
readonly operation?: string;
|
|
44
|
+
readonly stage?: string;
|
|
45
|
+
readonly retryable?: boolean;
|
|
46
|
+
readonly provider?: string;
|
|
47
|
+
readonly itemId?: string;
|
|
48
|
+
readonly message?: string;
|
|
49
|
+
readonly details?: Readonly<Record<string, unknown>>;
|
|
50
|
+
}
|
|
51
|
+
export interface RpcError {
|
|
52
|
+
readonly code: number;
|
|
53
|
+
readonly message: string;
|
|
54
|
+
readonly data?: RpcErrorData;
|
|
55
|
+
}
|
|
56
|
+
export type RpcResult = InitializeResult | SearchResult | ReadResult | Readonly<Record<string, never>>;
|
|
57
|
+
export type RpcResponse = {
|
|
58
|
+
readonly jsonrpc: '2.0';
|
|
59
|
+
readonly id: RpcId;
|
|
60
|
+
readonly result: RpcResult;
|
|
61
|
+
} | {
|
|
62
|
+
readonly jsonrpc: '2.0';
|
|
63
|
+
readonly id: RpcId;
|
|
64
|
+
readonly error: RpcError;
|
|
65
|
+
};
|
|
66
|
+
export type RpcRequest = {
|
|
67
|
+
readonly jsonrpc: '2.0';
|
|
68
|
+
readonly id: RpcId;
|
|
69
|
+
readonly method: 'initialize';
|
|
70
|
+
readonly params: {
|
|
71
|
+
readonly protocolVersion: '1.0';
|
|
72
|
+
};
|
|
73
|
+
} | {
|
|
74
|
+
readonly jsonrpc: '2.0';
|
|
75
|
+
readonly id: RpcId;
|
|
76
|
+
readonly method: 'search';
|
|
77
|
+
readonly params: {
|
|
78
|
+
readonly query: string;
|
|
79
|
+
readonly namespaces: readonly string[];
|
|
80
|
+
readonly mode: SearchMode;
|
|
81
|
+
readonly limit: number;
|
|
82
|
+
};
|
|
83
|
+
} | {
|
|
84
|
+
readonly jsonrpc: '2.0';
|
|
85
|
+
readonly id: RpcId;
|
|
86
|
+
readonly method: 'read';
|
|
87
|
+
readonly params: {
|
|
88
|
+
readonly evidenceRef: string;
|
|
89
|
+
readonly offset: number;
|
|
90
|
+
readonly maxChars: number;
|
|
91
|
+
readonly includeEvents: boolean;
|
|
92
|
+
};
|
|
93
|
+
} | {
|
|
94
|
+
readonly jsonrpc: '2.0';
|
|
95
|
+
readonly id: RpcId;
|
|
96
|
+
readonly method: 'shutdown';
|
|
97
|
+
readonly params: Readonly<Record<string, never>>;
|
|
98
|
+
} | {
|
|
99
|
+
readonly jsonrpc: '2.0';
|
|
100
|
+
readonly method: '$/cancelRequest';
|
|
101
|
+
readonly params: {
|
|
102
|
+
readonly id: RpcId;
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
/** A safe protocol failure that identifies only the frame correlation point. */
|
|
106
|
+
export declare class SagProtocolError extends Error {
|
|
107
|
+
readonly name = "SagProtocolError";
|
|
108
|
+
}
|
|
109
|
+
/** Decode and validate one JSON-RPC response frame from the Python process. */
|
|
110
|
+
export declare function decodeResponse(line: string): RpcResponse;
|
|
111
|
+
/** Encode one validated request as a single NDJSON frame. */
|
|
112
|
+
export declare function encodeRequest(request: RpcRequest): string;
|
|
113
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
export const RPC_PROTOCOL_VERSION = '1.0';
|
|
3
|
+
export const REQUIRED_ENGINE_VERSION = '0.10.0';
|
|
4
|
+
export const MAX_FRAME_BYTES = 8 * 1024 * 1024;
|
|
5
|
+
/** A safe protocol failure that identifies only the frame correlation point. */
|
|
6
|
+
export class SagProtocolError extends Error {
|
|
7
|
+
name = 'SagProtocolError';
|
|
8
|
+
}
|
|
9
|
+
function object(value, label) {
|
|
10
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
11
|
+
throw new SagProtocolError(`${label} must be an object`);
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function exactKeys(value, required, optional, label) {
|
|
16
|
+
const allowed = new Set([...required, ...optional]);
|
|
17
|
+
if (required.some(key => !(key in value)) || Object.keys(value).some(key => !allowed.has(key))) {
|
|
18
|
+
throw new SagProtocolError(`${label} contains invalid fields`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function validId(value) {
|
|
22
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
23
|
+
}
|
|
24
|
+
function validateResult(value, id) {
|
|
25
|
+
const result = object(value, `response id ${id} result`);
|
|
26
|
+
const keys = Object.keys(result);
|
|
27
|
+
if (keys.length === 0)
|
|
28
|
+
return {};
|
|
29
|
+
if ('protocolVersion' in result) {
|
|
30
|
+
exactKeys(result, ['protocolVersion', 'engineVersion', 'health', 'evidenceRead', 'namespaces'], [], `response id ${id} initialize result`);
|
|
31
|
+
if (typeof result.protocolVersion !== 'string' || typeof result.engineVersion !== 'string'
|
|
32
|
+
|| !['available', 'degraded', 'unavailable'].includes(String(result.health))
|
|
33
|
+
|| typeof result.evidenceRead !== 'boolean'
|
|
34
|
+
|| !Array.isArray(result.namespaces) || !result.namespaces.every(item => typeof item === 'string')) {
|
|
35
|
+
throw new SagProtocolError(`response id ${id} initialize result is invalid`);
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
if ('evidences' in result) {
|
|
40
|
+
exactKeys(result, ['query', 'evidences'], [], `response id ${id} search result`);
|
|
41
|
+
if (typeof result.query !== 'string' || !Array.isArray(result.evidences)) {
|
|
42
|
+
throw new SagProtocolError(`response id ${id} search result is invalid`);
|
|
43
|
+
}
|
|
44
|
+
for (const raw of result.evidences) {
|
|
45
|
+
const evidence = object(raw, `response id ${id} evidence`);
|
|
46
|
+
exactKeys(evidence, ['evidenceRef', 'namespaceId', 'sourceId', 'title', 'excerpt'], ['score'], `response id ${id} evidence`);
|
|
47
|
+
if (['evidenceRef', 'namespaceId', 'sourceId', 'title', 'excerpt'].some(key => typeof evidence[key] !== 'string')
|
|
48
|
+
|| ('score' in evidence && typeof evidence.score !== 'number')) {
|
|
49
|
+
throw new SagProtocolError(`response id ${id} evidence is invalid`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
if ('content' in result) {
|
|
55
|
+
exactKeys(result, ['title', 'content', 'offset', 'totalChars'], ['nextOffset', 'events'], `response id ${id} read result`);
|
|
56
|
+
if (typeof result.title !== 'string' || typeof result.content !== 'string'
|
|
57
|
+
|| !Number.isSafeInteger(result.offset) || result.offset < 0
|
|
58
|
+
|| !Number.isSafeInteger(result.totalChars) || result.totalChars < 0
|
|
59
|
+
|| ('nextOffset' in result && (!Number.isSafeInteger(result.nextOffset) || result.nextOffset < 0))
|
|
60
|
+
|| ('events' in result && !Array.isArray(result.events))) {
|
|
61
|
+
throw new SagProtocolError(`response id ${id} read result is invalid`);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
throw new SagProtocolError(`response id ${id} result contains invalid fields`);
|
|
66
|
+
}
|
|
67
|
+
/** Decode and validate one JSON-RPC response frame from the Python process. */
|
|
68
|
+
export function decodeResponse(line) {
|
|
69
|
+
if (Buffer.byteLength(line, 'utf8') > MAX_FRAME_BYTES) {
|
|
70
|
+
throw new SagProtocolError('response frame exceeds 8 MiB');
|
|
71
|
+
}
|
|
72
|
+
let raw;
|
|
73
|
+
try {
|
|
74
|
+
raw = JSON.parse(line);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
throw new SagProtocolError('response frame is not valid JSON');
|
|
78
|
+
}
|
|
79
|
+
const response = object(raw, 'response');
|
|
80
|
+
const idLabel = validId(response.id) ? ` id ${response.id}` : '';
|
|
81
|
+
exactKeys(response, ['jsonrpc', 'id'], ['result', 'error'], `response${idLabel}`);
|
|
82
|
+
if (response.jsonrpc !== '2.0' || !validId(response.id)) {
|
|
83
|
+
throw new SagProtocolError(`response${idLabel} has invalid JSON-RPC metadata`);
|
|
84
|
+
}
|
|
85
|
+
if (('result' in response) === ('error' in response)) {
|
|
86
|
+
throw new SagProtocolError(`response id ${response.id} must contain exactly one of result or error`);
|
|
87
|
+
}
|
|
88
|
+
if ('result' in response) {
|
|
89
|
+
return { jsonrpc: '2.0', id: response.id, result: validateResult(response.result, response.id) };
|
|
90
|
+
}
|
|
91
|
+
const error = object(response.error, `response id ${response.id} error`);
|
|
92
|
+
exactKeys(error, ['code', 'message'], ['data'], `response id ${response.id} error`);
|
|
93
|
+
if (!Number.isInteger(error.code) || typeof error.message !== 'string') {
|
|
94
|
+
throw new SagProtocolError(`response id ${response.id} error is invalid`);
|
|
95
|
+
}
|
|
96
|
+
let data;
|
|
97
|
+
if ('data' in error) {
|
|
98
|
+
const rawData = object(error.data, `response id ${response.id} error data`);
|
|
99
|
+
exactKeys(rawData, ['code'], ['operation', 'stage', 'retryable', 'provider', 'itemId', 'message', 'details'], `response id ${response.id} error data`);
|
|
100
|
+
if (typeof rawData.code !== 'string')
|
|
101
|
+
throw new SagProtocolError(`response id ${response.id} error data is invalid`);
|
|
102
|
+
data = rawData;
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
jsonrpc: '2.0',
|
|
106
|
+
id: response.id,
|
|
107
|
+
error: { code: error.code, message: error.message, ...(data ? { data } : {}) },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** Encode one validated request as a single NDJSON frame. */
|
|
111
|
+
export function encodeRequest(request) {
|
|
112
|
+
const line = `${JSON.stringify(request)}\n`;
|
|
113
|
+
if (Buffer.byteLength(line, 'utf8') > MAX_FRAME_BYTES) {
|
|
114
|
+
throw new SagProtocolError('request frame exceeds 8 MiB');
|
|
115
|
+
}
|
|
116
|
+
return line;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SubprocessRuntime } from '@deepseek-ai/dsh-subprocess';
|
|
2
|
+
import { SagRuntimeClient } from './client.js';
|
|
3
|
+
export interface SupervisorConfig {
|
|
4
|
+
readonly pythonCommand: string;
|
|
5
|
+
readonly envFile: string;
|
|
6
|
+
readonly cwd: string;
|
|
7
|
+
readonly namespaces: readonly {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly label: string;
|
|
10
|
+
}[];
|
|
11
|
+
readonly maxReadEngines: number;
|
|
12
|
+
readonly maxResults: number;
|
|
13
|
+
readonly maxSnippetChars: number;
|
|
14
|
+
readonly maxReadChars: number;
|
|
15
|
+
readonly shutdownGraceMs: number;
|
|
16
|
+
readonly allowDegraded: boolean;
|
|
17
|
+
}
|
|
18
|
+
type RuntimeLauncher = Pick<SubprocessRuntime, 'resolveExecutable' | 'spawn'>;
|
|
19
|
+
/** Owns one Python sidecar from executable resolution through tree quiescence. */
|
|
20
|
+
export declare class SagRuntimeSupervisor {
|
|
21
|
+
#private;
|
|
22
|
+
readonly client: SagRuntimeClient;
|
|
23
|
+
private constructor();
|
|
24
|
+
/** Spawn and verify a sidecar before exposing its client to tool registration. */
|
|
25
|
+
static start(runtime: RuntimeLauncher, config: SupervisorConfig): Promise<SagRuntimeSupervisor>;
|
|
26
|
+
/** Gracefully stop admission, then escalate through the managed process seam. */
|
|
27
|
+
dispose(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=supervisor.d.ts.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { SagRuntimeClient } from './client.js';
|
|
2
|
+
import { REQUIRED_ENGINE_VERSION, RPC_PROTOCOL_VERSION, } from './protocol.js';
|
|
3
|
+
function initializeResult(result) {
|
|
4
|
+
if (!('protocolVersion' in result) || !('engineVersion' in result) || !('namespaces' in result)) {
|
|
5
|
+
throw new Error('SAG runtime initialize returned the wrong result type');
|
|
6
|
+
}
|
|
7
|
+
return result;
|
|
8
|
+
}
|
|
9
|
+
/** Owns one Python sidecar from executable resolution through tree quiescence. */
|
|
10
|
+
export class SagRuntimeSupervisor {
|
|
11
|
+
client;
|
|
12
|
+
#handle;
|
|
13
|
+
#graceMs;
|
|
14
|
+
#disposed = false;
|
|
15
|
+
constructor(handle, client, graceMs) {
|
|
16
|
+
this.#handle = handle;
|
|
17
|
+
this.client = client;
|
|
18
|
+
this.#graceMs = graceMs;
|
|
19
|
+
}
|
|
20
|
+
/** Spawn and verify a sidecar before exposing its client to tool registration. */
|
|
21
|
+
static async start(runtime, config) {
|
|
22
|
+
const python = await runtime.resolveExecutable(config.pythonCommand, { PYTHONUNBUFFERED: '1' });
|
|
23
|
+
const namespaceArgs = config.namespaces.flatMap(namespace => ['--namespace', namespace.id]);
|
|
24
|
+
const handle = runtime.spawn({
|
|
25
|
+
argv: [
|
|
26
|
+
python,
|
|
27
|
+
'-m',
|
|
28
|
+
'dsh_sag_runtime',
|
|
29
|
+
'--env-file',
|
|
30
|
+
config.envFile,
|
|
31
|
+
...namespaceArgs,
|
|
32
|
+
'--max-read-engines',
|
|
33
|
+
String(config.maxReadEngines),
|
|
34
|
+
'--max-results',
|
|
35
|
+
String(config.maxResults),
|
|
36
|
+
'--max-excerpt-chars',
|
|
37
|
+
String(config.maxSnippetChars),
|
|
38
|
+
'--max-read-chars',
|
|
39
|
+
String(config.maxReadChars),
|
|
40
|
+
],
|
|
41
|
+
cwd: config.cwd,
|
|
42
|
+
stdio: {
|
|
43
|
+
stdin: 'pipe',
|
|
44
|
+
stdout: 'pipe',
|
|
45
|
+
stderr: { maxBytes: 64 * 1024 },
|
|
46
|
+
},
|
|
47
|
+
graceMs: config.shutdownGraceMs,
|
|
48
|
+
env: { PYTHONUNBUFFERED: '1' },
|
|
49
|
+
});
|
|
50
|
+
if (!handle.stdin || !handle.stdout) {
|
|
51
|
+
handle.terminate();
|
|
52
|
+
throw new Error('SAG runtime did not expose piped stdin/stdout');
|
|
53
|
+
}
|
|
54
|
+
const client = new SagRuntimeClient(handle.stdin, handle.stdout, handle.done);
|
|
55
|
+
const supervisor = new SagRuntimeSupervisor(handle, client, config.shutdownGraceMs);
|
|
56
|
+
try {
|
|
57
|
+
const ready = initializeResult(await client.request('initialize', { protocolVersion: RPC_PROTOCOL_VERSION }));
|
|
58
|
+
if (ready.protocolVersion !== RPC_PROTOCOL_VERSION) {
|
|
59
|
+
throw new Error(`SAG runtime protocol ${RPC_PROTOCOL_VERSION} is required`);
|
|
60
|
+
}
|
|
61
|
+
if (ready.engineVersion !== REQUIRED_ENGINE_VERSION) {
|
|
62
|
+
throw new Error(`zleap-sag ${REQUIRED_ENGINE_VERSION} is required; sidecar reported ${ready.engineVersion}`);
|
|
63
|
+
}
|
|
64
|
+
if (!ready.evidenceRead)
|
|
65
|
+
throw new Error('SAG runtime does not support evidence reads');
|
|
66
|
+
const expectedNamespaces = config.namespaces.map(namespace => namespace.id);
|
|
67
|
+
if (JSON.stringify(ready.namespaces) !== JSON.stringify(expectedNamespaces)) {
|
|
68
|
+
throw new Error('SAG runtime namespace handshake does not match plugin configuration');
|
|
69
|
+
}
|
|
70
|
+
if (ready.health === 'unavailable' || (ready.health === 'degraded' && !config.allowDegraded)) {
|
|
71
|
+
throw new Error(`SAG runtime health is ${ready.health}`);
|
|
72
|
+
}
|
|
73
|
+
return supervisor;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
handle.terminate();
|
|
77
|
+
client.closeInput();
|
|
78
|
+
await handle.waitForExit();
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Gracefully stop admission, then escalate through the managed process seam. */
|
|
83
|
+
async dispose() {
|
|
84
|
+
if (this.#disposed)
|
|
85
|
+
return;
|
|
86
|
+
this.#disposed = true;
|
|
87
|
+
let timer;
|
|
88
|
+
const timeout = new Promise(resolve => {
|
|
89
|
+
timer = setTimeout(() => resolve('timeout'), this.#graceMs);
|
|
90
|
+
});
|
|
91
|
+
const shutdown = this.client.request('shutdown', {}).then(() => 'shutdown');
|
|
92
|
+
const outcome = await Promise.race([shutdown, timeout]).catch(() => 'failure');
|
|
93
|
+
if (timer)
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
this.client.closeInput();
|
|
96
|
+
if (outcome !== 'shutdown')
|
|
97
|
+
this.#handle.terminate();
|
|
98
|
+
const signal = AbortSignal.timeout(this.#graceMs);
|
|
99
|
+
const exited = await this.#handle.waitForExit(signal);
|
|
100
|
+
if (!exited) {
|
|
101
|
+
this.#handle.terminate();
|
|
102
|
+
await this.#handle.waitForExit();
|
|
103
|
+
}
|
|
104
|
+
await this.#handle.done;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=supervisor.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ToolExecution, PreToolDecision } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import type { ResolvedLocalConfig } from '../config.js';
|
|
3
|
+
import { type ConnectionManager } from './local.js';
|
|
4
|
+
/** Require user approval for irreversible SAG document deletion. */
|
|
5
|
+
export declare function sagDeleteApprovalGate(exec: Pick<ToolExecution, 'name'>, next: () => Promise<PreToolDecision>): Promise<PreToolDecision>;
|
|
6
|
+
/** Build local SAG document inspection, reprocessing, and deletion tools. */
|
|
7
|
+
export declare function createDocumentTools(manager: ConnectionManager, config: ResolvedLocalConfig): readonly [import("@deepseek-ai/dsh-tools").ToolDefinition, import("@deepseek-ai/dsh-tools").ToolDefinition, import("@deepseek-ai/dsh-tools").ToolDefinition, import("@deepseek-ai/dsh-tools").ToolDefinition];
|
|
8
|
+
//# sourceMappingURL=documents.d.ts.map
|