@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +58 -0
  4. package/README.zh.md +58 -0
  5. package/THIRD_PARTY_NOTICES +671 -0
  6. package/cordis.patch.yml +5 -0
  7. package/docs/embedded.md +61 -0
  8. package/lib/brand.d.ts +13 -0
  9. package/lib/brand.js +15 -0
  10. package/lib/cli/runtime.d.ts +40 -0
  11. package/lib/cli/runtime.js +121 -0
  12. package/lib/cli.d.ts +21 -0
  13. package/lib/cli.js +265 -0
  14. package/lib/config.d.ts +60 -0
  15. package/lib/config.js +120 -0
  16. package/lib/connection/descriptor.d.ts +4 -0
  17. package/lib/connection/descriptor.js +66 -0
  18. package/lib/connection/discovery.d.ts +40 -0
  19. package/lib/connection/discovery.js +139 -0
  20. package/lib/connection/guidance.d.ts +7 -0
  21. package/lib/connection/guidance.js +7 -0
  22. package/lib/connection/manager.d.ts +67 -0
  23. package/lib/connection/manager.js +273 -0
  24. package/lib/connection/store.d.ts +42 -0
  25. package/lib/connection/store.js +164 -0
  26. package/lib/connection/types.d.ts +35 -0
  27. package/lib/connection/types.js +2 -0
  28. package/lib/dsh-sag-cli.js +32202 -0
  29. package/lib/index.d.ts +11 -0
  30. package/lib/index.js +68 -0
  31. package/lib/local/api-client.d.ts +157 -0
  32. package/lib/local/api-client.js +338 -0
  33. package/lib/local/gateway.d.ts +43 -0
  34. package/lib/local/gateway.js +34 -0
  35. package/lib/local/mcp-probe.d.ts +27 -0
  36. package/lib/local/mcp-probe.js +92 -0
  37. package/lib/presentation.d.ts +8 -0
  38. package/lib/presentation.js +7 -0
  39. package/lib/runtime/client.d.ts +22 -0
  40. package/lib/runtime/client.js +117 -0
  41. package/lib/runtime/protocol.d.ts +113 -0
  42. package/lib/runtime/protocol.js +118 -0
  43. package/lib/runtime/supervisor.d.ts +30 -0
  44. package/lib/runtime/supervisor.js +107 -0
  45. package/lib/tools/documents.d.ts +8 -0
  46. package/lib/tools/documents.js +134 -0
  47. package/lib/tools/ingest.d.ts +5 -0
  48. package/lib/tools/ingest.js +35 -0
  49. package/lib/tools/local.d.ts +40 -0
  50. package/lib/tools/local.js +111 -0
  51. package/lib/tools/output.d.ts +96 -0
  52. package/lib/tools/output.js +67 -0
  53. package/lib/tools/read.d.ts +6 -0
  54. package/lib/tools/read.js +84 -0
  55. package/lib/tools/search.d.ts +6 -0
  56. package/lib/tools/search.js +107 -0
  57. package/lib/tools/sources.d.ts +5 -0
  58. package/lib/tools/sources.js +61 -0
  59. package/lib/tools/status.d.ts +5 -0
  60. package/lib/tools/status.js +28 -0
  61. package/lib/tools/upload.d.ts +6 -0
  62. package/lib/tools/upload.js +68 -0
  63. package/package.json +96 -0
  64. package/runtime/pyproject.toml +29 -0
  65. package/runtime/src/dsh_sag_runtime/__init__.py +3 -0
  66. package/runtime/src/dsh_sag_runtime/__main__.py +80 -0
  67. package/runtime/src/dsh_sag_runtime/engines.py +139 -0
  68. package/runtime/src/dsh_sag_runtime/errors.py +47 -0
  69. package/runtime/src/dsh_sag_runtime/evidence.py +53 -0
  70. package/runtime/src/dsh_sag_runtime/protocol.py +161 -0
  71. package/runtime/src/dsh_sag_runtime/read.py +61 -0
  72. package/runtime/src/dsh_sag_runtime/search.py +76 -0
  73. package/runtime/src/dsh_sag_runtime/server.py +136 -0
  74. package/runtime/uv.lock +2310 -0
  75. package/scripts/setup-runtime.mjs +47 -0
package/lib/index.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { Config, type Config as PluginConfig } from './config.js';
3
+ export { Config };
4
+ export type { PluginConfig as ConfigType };
5
+ export declare const name = "dsh-sag";
6
+ export declare const inject: string[];
7
+ export declare const SAG_SYSTEM_PROMPT = "Use sag_search to find evidence in configured SAG knowledge namespaces. Use sag_read with an evidence_ref from sag_search when you need the full bounded source text. Treat evidence_ref as opaque and continue paged reads with the returned next offset.";
8
+ export declare const SAG_LOCAL_SYSTEM_PROMPT = "Use SAG as the user's local personal knowledge base. Use sag_status or sag_list_sources when the target knowledge base is unclear, sag_search before sag_read for evidence, and the source and document tools to create, upload, ingest, inspect, reprocess, or delete content. Treat evidence_ref as opaque and continue paged reads with the returned next offset. Document deletion requires user approval.";
9
+ /** Assemble the local connector by default or the explicit legacy embedded sidecar. */
10
+ export declare function apply(ctx: Context, config: PluginConfig): Promise<void>;
11
+ //# sourceMappingURL=index.d.ts.map
package/lib/index.js ADDED
@@ -0,0 +1,68 @@
1
+ import { homedir } from 'node:os';
2
+ import { Config, resolveConfig } from './config.js';
3
+ import { discoverConnection, platformConnectionPaths } from './connection/discovery.js';
4
+ import { SagConnectionManager } from './connection/manager.js';
5
+ import { registerSagSettings, SagConnectionStore } from './connection/store.js';
6
+ import { SagRuntimeSupervisor } from './runtime/supervisor.js';
7
+ import { createDocumentTools, sagDeleteApprovalGate } from './tools/documents.js';
8
+ import { createIngestTextTool } from './tools/ingest.js';
9
+ import { createReadTool } from './tools/read.js';
10
+ import { createSearchTool } from './tools/search.js';
11
+ import { createSourceTools } from './tools/sources.js';
12
+ import { createStatusTool } from './tools/status.js';
13
+ import { createUploadTool } from './tools/upload.js';
14
+ export { Config };
15
+ export const name = 'dsh-sag';
16
+ export const inject = ['tools', 'systemPrompt'];
17
+ export const SAG_SYSTEM_PROMPT = 'Use sag_search to find evidence in configured SAG knowledge namespaces. Use sag_read with an evidence_ref from sag_search when you need the full bounded source text. Treat evidence_ref as opaque and continue paged reads with the returned next offset.';
18
+ export const SAG_LOCAL_SYSTEM_PROMPT = 'Use SAG as the user\'s local personal knowledge base. Use sag_status or sag_list_sources when the target knowledge base is unclear, sag_search before sag_read for evidence, and the source and document tools to create, upload, ingest, inspect, reprocess, or delete content. Treat evidence_ref as opaque and continue paged reads with the returned next offset. Document deletion requires user approval.';
19
+ async function applyEmbedded(ctx, config) {
20
+ await ctx.effect(async function* () {
21
+ const supervisor = await SagRuntimeSupervisor.start(ctx.subprocess, config);
22
+ yield async () => supervisor.dispose();
23
+ ctx.systemPrompt.section({ name: 'tool:dsh-sag', order: 112, text: SAG_SYSTEM_PROMPT });
24
+ ctx.tools.register(createSearchTool(supervisor.client, config));
25
+ ctx.tools.register(createReadTool(supervisor.client, config));
26
+ }, 'dsh-sag: embedded sidecar lifecycle');
27
+ }
28
+ async function applyLocal(ctx, config) {
29
+ await ctx.effect(async function* () {
30
+ const settings = registerSagSettings(ctx);
31
+ const store = new SagConnectionStore({ credentials: ctx.credentials, settings });
32
+ const manager = new SagConnectionManager({
33
+ store,
34
+ requestTimeoutMs: config.requestTimeoutMs,
35
+ readyCacheTtlMs: config.connectionCacheTtlMs,
36
+ discover: signal => discoverConnection({
37
+ fs: ctx.fs,
38
+ paths: platformConnectionPaths(process.env, process.platform, homedir()),
39
+ urls: config.discoveryUrls,
40
+ fetch: (url, init) => fetch(url, init),
41
+ }, signal),
42
+ });
43
+ const tools = [
44
+ createStatusTool(manager, config),
45
+ ...createSourceTools(manager, config),
46
+ createSearchTool(manager, config),
47
+ createReadTool(manager, config),
48
+ ...createDocumentTools(manager, config),
49
+ createUploadTool(manager, config, ctx.fs),
50
+ createIngestTextTool(manager, config),
51
+ ];
52
+ ctx.systemPrompt.section({ name: 'tool:dsh-sag', order: 112, text: SAG_LOCAL_SYSTEM_PROMPT });
53
+ for (const tool of tools)
54
+ ctx.tools.register(tool);
55
+ ctx.on('tools/pre-execute', sagDeleteApprovalGate);
56
+ yield () => undefined;
57
+ }, 'dsh-sag: local connection and tools');
58
+ }
59
+ /** Assemble the local connector by default or the explicit legacy embedded sidecar. */
60
+ export async function apply(ctx, config) {
61
+ const resolved = resolveConfig(config);
62
+ if (resolved.mode === 'embedded') {
63
+ await ctx.inject(['subprocess'], child => applyEmbedded(child, resolved));
64
+ return;
65
+ }
66
+ await ctx.inject(['settings', 'credentials', 'fs'], child => applyLocal(child, resolved));
67
+ }
68
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,157 @@
1
+ import type { SagCapabilityDescriptor, SagConnectionDescriptor } from '../connection/types.js';
2
+ /** One SAG source projected through its public REST fields. */
3
+ export interface SagSource {
4
+ readonly id: string;
5
+ readonly name: string;
6
+ readonly description?: string;
7
+ readonly status?: string;
8
+ readonly documentCount?: number;
9
+ readonly chunkCount?: number;
10
+ readonly [field: string]: unknown;
11
+ }
12
+ /** Parameters accepted by SAG's source creation endpoint. */
13
+ export interface CreateSourceRequest {
14
+ readonly name: string;
15
+ readonly description?: string;
16
+ readonly connectorKind?: string;
17
+ readonly config?: Readonly<Record<string, unknown>>;
18
+ }
19
+ /** Parameters for structured workspace search. */
20
+ export interface SagSearchRequest {
21
+ readonly query: string;
22
+ readonly sourceIds?: readonly string[];
23
+ readonly topK?: number;
24
+ readonly strategy?: string;
25
+ }
26
+ /** One structured evidence section returned by SAG. */
27
+ export interface SagSearchSection {
28
+ readonly sourceId: string;
29
+ readonly sourceName: string | null;
30
+ readonly chunkId: string;
31
+ readonly heading: string;
32
+ readonly content: string;
33
+ readonly score: number;
34
+ readonly rank: number;
35
+ }
36
+ /** Structured search response used by the local gateway. */
37
+ export interface SagSearchResult {
38
+ readonly query: string;
39
+ readonly sections: readonly SagSearchSection[];
40
+ readonly summary: string;
41
+ readonly stats: Readonly<Record<string, unknown>>;
42
+ }
43
+ /** Bytes already admitted by dsh FS for one multipart upload. */
44
+ export interface SagFileUpload {
45
+ readonly sourceId: string;
46
+ readonly filename: string;
47
+ readonly contentType: string;
48
+ readonly bytes: Uint8Array;
49
+ }
50
+ /** Text content to ingest into one source. */
51
+ export interface SagTextIngest {
52
+ readonly sourceId: string;
53
+ readonly text: string;
54
+ readonly title?: string;
55
+ readonly messages?: readonly Readonly<Record<string, unknown>>[];
56
+ }
57
+ /** A source-bound SAG document or processing job. */
58
+ export interface SagDocument {
59
+ readonly id: string;
60
+ readonly sourceId?: string;
61
+ readonly filename?: string;
62
+ readonly status?: 'pending' | 'loading' | 'extracting' | 'pausing' | 'paused' | 'deleting' | 'delete_failed' | 'ready' | 'failed';
63
+ readonly progress?: number;
64
+ readonly chunkCount?: number;
65
+ readonly error?: string | null;
66
+ readonly errorLayer?: string | null;
67
+ readonly errorStage?: string | null;
68
+ readonly [field: string]: unknown;
69
+ }
70
+ /** One asynchronous SAG processing job returned by document mutations. */
71
+ export interface SagJob {
72
+ readonly id: string;
73
+ readonly status: 'queued' | 'running' | 'paused' | 'succeeded' | 'failed';
74
+ readonly sourceId: string | null;
75
+ readonly documentId: string | null;
76
+ readonly type: 'process_document' | 'reprocess_document' | 'delete_document' | 'sync_source' | 'index_universe' | 'octx_preflight' | 'octx_import' | 'octx_export' | 'octx_gc_installation' | 'octx_gc_transfer';
77
+ readonly progress?: number;
78
+ readonly attempts?: number;
79
+ readonly error?: string | null;
80
+ }
81
+ /** Reprocess endpoint job with its operation-specific type. */
82
+ export interface SagReprocessJob extends SagJob {
83
+ readonly type: 'reprocess_document';
84
+ }
85
+ /** Full content for one source chunk. */
86
+ export interface SagChunk {
87
+ readonly sourceId: string;
88
+ readonly chunkId: string;
89
+ readonly content: string;
90
+ readonly [field: string]: unknown;
91
+ }
92
+ /** Versioned local reference emitted by search and accepted by read. */
93
+ export interface LocalEvidenceRef {
94
+ readonly v: 1;
95
+ readonly sourceId: string;
96
+ readonly chunkId: string;
97
+ }
98
+ /** A non-success SAG response with its stable public error fields. */
99
+ export declare class SagApiError extends Error {
100
+ readonly status: number;
101
+ readonly code: string;
102
+ readonly retryable: boolean | undefined;
103
+ readonly requestId: string | undefined;
104
+ readonly layer: string | undefined;
105
+ readonly stage: string | undefined;
106
+ /**
107
+ * @param status - HTTP status returned by SAG.
108
+ * @param code - stable SAG error code.
109
+ * @param message - redacted public error message.
110
+ * @param retryable - whether SAG considers a retry safe.
111
+ * @param requestId - optional SAG request identifier.
112
+ * @param layer - public SAG responsibility category.
113
+ * @param stage - public SAG processing stage.
114
+ */
115
+ constructor(status: number, code: string, message: string, retryable: boolean | undefined, requestId: string | undefined, layer: string | undefined, stage: string | undefined);
116
+ }
117
+ /** Encode a canonical local SAG evidence reference. */
118
+ export declare function encodeLocalEvidenceRef(ref: LocalEvidenceRef): string;
119
+ /** Decode and strictly validate a canonical local SAG evidence reference. */
120
+ export declare function decodeLocalEvidenceRef(encoded: string): LocalEvidenceRef;
121
+ /** REST client for the public SAG dsh, source, search, chunk, and document endpoints. */
122
+ export declare class SagApiClient {
123
+ private readonly descriptor;
124
+ private readonly fetch;
125
+ /** @param descriptor - resolved local SAG connection. @param fetch - injectable HTTP implementation. */
126
+ constructor(descriptor: SagConnectionDescriptor, fetch?: typeof globalThis.fetch);
127
+ private requestJson;
128
+ private redact;
129
+ private json;
130
+ /** Check whether the SAG API process is alive. */
131
+ health(signal: AbortSignal): Promise<unknown>;
132
+ /** Check whether SAG storage and knowledge runtime are ready. */
133
+ ready(signal: AbortSignal): Promise<unknown>;
134
+ /** Read the versioned dsh integration capabilities. */
135
+ capabilities(signal: AbortSignal): Promise<SagCapabilityDescriptor>;
136
+ /** List all local knowledge sources. */
137
+ listSources(signal: AbortSignal): Promise<readonly SagSource[]>;
138
+ /** Create one file-upload or configured SAG source. */
139
+ createSource(request: CreateSourceRequest, signal: AbortSignal): Promise<SagSource>;
140
+ /** Search one or more sources using SAG's structured workspace response. */
141
+ search(request: SagSearchRequest, signal: AbortSignal): Promise<SagSearchResult>;
142
+ /** Read one complete chunk by its source-local identifiers. */
143
+ readChunk(sourceId: string, chunkId: string, signal: AbortSignal): Promise<SagChunk>;
144
+ /** List documents belonging to one source. */
145
+ listDocuments(sourceId: string, signal: AbortSignal): Promise<readonly SagDocument[]>;
146
+ /** Get one document and its current processing status. */
147
+ getDocument(sourceId: string, documentId: string, signal: AbortSignal): Promise<SagDocument>;
148
+ /** Upload bytes already read through dsh FS as one multipart document. */
149
+ uploadFile(upload: SagFileUpload, signal: AbortSignal): Promise<SagDocument>;
150
+ /** Ingest text or messages without waiting for background parsing. */
151
+ ingestText(ingest: SagTextIngest, signal: AbortSignal): Promise<SagDocument>;
152
+ /** Queue reprocessing for one document. */
153
+ reprocessDocument(sourceId: string, documentId: string, signal: AbortSignal): Promise<SagReprocessJob>;
154
+ /** Delete one document after the caller has obtained dsh approval. */
155
+ deleteDocument(sourceId: string, documentId: string, signal: AbortSignal): Promise<unknown>;
156
+ }
157
+ //# sourceMappingURL=api-client.d.ts.map
@@ -0,0 +1,338 @@
1
+ /** A non-success SAG response with its stable public error fields. */
2
+ export class SagApiError extends Error {
3
+ status;
4
+ code;
5
+ retryable;
6
+ requestId;
7
+ layer;
8
+ stage;
9
+ /**
10
+ * @param status - HTTP status returned by SAG.
11
+ * @param code - stable SAG error code.
12
+ * @param message - redacted public error message.
13
+ * @param retryable - whether SAG considers a retry safe.
14
+ * @param requestId - optional SAG request identifier.
15
+ * @param layer - public SAG responsibility category.
16
+ * @param stage - public SAG processing stage.
17
+ */
18
+ constructor(status, code, message, retryable, requestId, layer, stage) {
19
+ super(`SAG API ${status} ${code}: ${message}`);
20
+ this.status = status;
21
+ this.code = code;
22
+ this.retryable = retryable;
23
+ this.requestId = requestId;
24
+ this.layer = layer;
25
+ this.stage = stage;
26
+ this.name = 'SagApiError';
27
+ }
28
+ }
29
+ function object(value, label) {
30
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
31
+ throw new Error(`dsh-sag: ${label} must be an object`);
32
+ return value;
33
+ }
34
+ function string(value, label) {
35
+ if (typeof value !== 'string' || !value)
36
+ throw new Error(`dsh-sag: ${label} must be a non-empty string`);
37
+ return value;
38
+ }
39
+ function number(value, label) {
40
+ if (typeof value !== 'number' || !Number.isFinite(value))
41
+ throw new Error(`dsh-sag: ${label} must be a finite number`);
42
+ return value;
43
+ }
44
+ function recordWithCamelIds(value, label) {
45
+ const source = object(value, label);
46
+ const result = { ...source };
47
+ if (typeof source.source_id === 'string')
48
+ result.sourceId = source.source_id;
49
+ if (typeof source.chunk_id === 'string')
50
+ result.chunkId = source.chunk_id;
51
+ if (typeof source.document_count === 'number')
52
+ result.documentCount = source.document_count;
53
+ if (typeof source.chunk_count === 'number')
54
+ result.chunkCount = source.chunk_count;
55
+ if (source.error === null || typeof source.error === 'string')
56
+ result.error = source.error;
57
+ if (source.error_layer === null || typeof source.error_layer === 'string')
58
+ result.errorLayer = source.error_layer;
59
+ if (source.error_stage === null || typeof source.error_stage === 'string')
60
+ result.errorStage = source.error_stage;
61
+ return result;
62
+ }
63
+ function parseSource(value) {
64
+ const result = recordWithCamelIds(value, 'source');
65
+ return { ...result, id: string(result.id, 'source.id'), name: string(result.name, 'source.name') };
66
+ }
67
+ function parseDocument(value) {
68
+ const result = recordWithCamelIds(value, 'document');
69
+ if (result.progress !== undefined && !Number.isInteger(result.progress))
70
+ throw new Error('dsh-sag: document.progress must be an integer percentage');
71
+ return { ...result, id: string(result.id, 'document.id') };
72
+ }
73
+ function nullableString(value, label) {
74
+ if (value === null)
75
+ return null;
76
+ return string(value, label);
77
+ }
78
+ function parseJob(value) {
79
+ const source = object(value, 'job');
80
+ const jobTypes = ['process_document', 'reprocess_document', 'delete_document', 'sync_source', 'index_universe', 'octx_preflight', 'octx_import', 'octx_export', 'octx_gc_installation', 'octx_gc_transfer'];
81
+ const jobStatuses = ['queued', 'running', 'paused', 'succeeded', 'failed'];
82
+ const type = string(source.type, 'job.type');
83
+ const status = string(source.status, 'job.status');
84
+ if (!jobTypes.includes(type))
85
+ throw new Error('dsh-sag: job.type is unsupported');
86
+ if (!jobStatuses.includes(status))
87
+ throw new Error('dsh-sag: job.status is unsupported');
88
+ const optionalProgress = typeof source.progress === 'number' ? { progress: source.progress } : {};
89
+ const optionalAttempts = typeof source.attempts === 'number' ? { attempts: source.attempts } : {};
90
+ const optionalError = source.error === null || typeof source.error === 'string' ? { error: source.error } : {};
91
+ return {
92
+ id: string(source.id, 'job.id'),
93
+ type: type,
94
+ status: status,
95
+ sourceId: nullableString(source.source_id, 'job.source_id'),
96
+ documentId: nullableString(source.document_id, 'job.document_id'),
97
+ ...optionalProgress,
98
+ ...optionalAttempts,
99
+ ...optionalError,
100
+ };
101
+ }
102
+ function parseCapability(value) {
103
+ const root = object(value, 'capability response');
104
+ if (root.schemaVersion !== 1)
105
+ throw new Error('dsh-sag: capability response requires schemaVersion 1');
106
+ if (!Array.isArray(root.capabilities) || !root.capabilities.every(item => typeof item === 'string')) {
107
+ throw new Error('dsh-sag: capability response capabilities must be strings');
108
+ }
109
+ let upload;
110
+ if (root.upload !== undefined) {
111
+ const candidate = object(root.upload, 'capability response upload');
112
+ if (!Number.isInteger(candidate.maxMb) || candidate.maxMb <= 0) {
113
+ throw new Error('dsh-sag: capability response upload.maxMb must be a positive integer');
114
+ }
115
+ if (!Array.isArray(candidate.extensions) || !candidate.extensions.every(item => typeof item === 'string' && item.length > 0 && !item.startsWith('.'))) {
116
+ throw new Error('dsh-sag: capability response upload.extensions must omit leading dots');
117
+ }
118
+ upload = { maxMb: candidate.maxMb, extensions: candidate.extensions };
119
+ }
120
+ const defaultSourceId = root.defaultSourceId;
121
+ if (defaultSourceId !== undefined && defaultSourceId !== null && (typeof defaultSourceId !== 'string' || !defaultSourceId)) {
122
+ throw new Error('dsh-sag: capability response defaultSourceId must be a non-empty string or null');
123
+ }
124
+ return {
125
+ schemaVersion: 1,
126
+ capabilities: root.capabilities,
127
+ ...(upload === undefined ? {} : { upload }),
128
+ ...(defaultSourceId === undefined ? {} : { defaultSourceId }),
129
+ };
130
+ }
131
+ function encodePath(value) {
132
+ return encodeURIComponent(value);
133
+ }
134
+ /** Encode a canonical local SAG evidence reference. */
135
+ export function encodeLocalEvidenceRef(ref) {
136
+ return Buffer.from(JSON.stringify({ v: 1, sourceId: ref.sourceId, chunkId: ref.chunkId }), 'utf8').toString('base64url');
137
+ }
138
+ /** Decode and strictly validate a canonical local SAG evidence reference. */
139
+ export function decodeLocalEvidenceRef(encoded) {
140
+ try {
141
+ if (encoded.length > 4096)
142
+ throw new Error('evidence reference is too long');
143
+ if (!encoded || !/^[A-Za-z0-9_-]+$/.test(encoded))
144
+ throw new Error('invalid alphabet');
145
+ const decoded = Buffer.from(encoded, 'base64url').toString('utf8');
146
+ const value = object(JSON.parse(decoded), 'evidence reference');
147
+ if (Object.keys(value).sort().join(',') !== 'chunkId,sourceId,v')
148
+ throw new Error('unexpected fields');
149
+ if (value.v !== 1)
150
+ throw new Error('unsupported version');
151
+ const ref = { v: 1, sourceId: string(value.sourceId, 'evidence reference sourceId'), chunkId: string(value.chunkId, 'evidence reference chunkId') };
152
+ if (encodeLocalEvidenceRef(ref) !== encoded)
153
+ throw new Error('non-canonical encoding');
154
+ return ref;
155
+ }
156
+ catch (error) {
157
+ throw new Error(`dsh-sag: invalid local evidence reference: ${error instanceof Error ? error.message : String(error)}`);
158
+ }
159
+ }
160
+ function requestSignal(init, signal) {
161
+ if (init.signal === undefined || init.signal === null || init.signal === signal)
162
+ return signal;
163
+ return AbortSignal.any([init.signal, signal]);
164
+ }
165
+ /** REST client for the public SAG dsh, source, search, chunk, and document endpoints. */
166
+ export class SagApiClient {
167
+ descriptor;
168
+ fetch;
169
+ /** @param descriptor - resolved local SAG connection. @param fetch - injectable HTTP implementation. */
170
+ constructor(descriptor, fetch = globalThis.fetch) {
171
+ this.descriptor = descriptor;
172
+ this.fetch = fetch;
173
+ }
174
+ async requestJson(path, init, signal) {
175
+ const headers = {};
176
+ if (init.headers instanceof Headers) {
177
+ for (const [name, value] of init.headers.entries())
178
+ headers[name] = value;
179
+ }
180
+ else if (Array.isArray(init.headers)) {
181
+ for (const [name, value] of init.headers)
182
+ headers[name] = value;
183
+ }
184
+ else if (init.headers !== undefined) {
185
+ Object.assign(headers, init.headers);
186
+ }
187
+ headers.Authorization = `Bearer ${this.descriptor.accessToken}`;
188
+ let response;
189
+ try {
190
+ response = await this.fetch(`${this.descriptor.apiUrl}${path}`, {
191
+ ...init,
192
+ headers,
193
+ signal: requestSignal(init, signal),
194
+ });
195
+ }
196
+ catch (error) {
197
+ if (signal.aborted)
198
+ throw signal.reason;
199
+ const message = error instanceof Error ? error.message : String(error);
200
+ throw new Error(`dsh-sag: SAG API request failed: ${this.redact(message)}`);
201
+ }
202
+ let payload;
203
+ try {
204
+ payload = await response.json();
205
+ }
206
+ catch {
207
+ if (!response.ok)
208
+ throw new SagApiError(response.status, 'http_error', this.redact(response.statusText || 'request failed'), undefined, undefined, undefined, undefined);
209
+ throw new Error(`dsh-sag: SAG API ${response.status} returned invalid JSON`);
210
+ }
211
+ if (!response.ok) {
212
+ const root = typeof payload === 'object' && payload !== null ? payload : {};
213
+ const candidate = typeof root.error === 'object' && root.error !== null ? root.error : {};
214
+ const parsed = {
215
+ code: typeof candidate.code === 'string' ? candidate.code : 'http_error',
216
+ message: typeof candidate.message === 'string' ? candidate.message : response.statusText || 'request failed',
217
+ ...(typeof candidate.retryable === 'boolean' ? { retryable: candidate.retryable } : {}),
218
+ ...(typeof candidate.request_id === 'string' ? { request_id: candidate.request_id } : {}),
219
+ ...(typeof candidate.layer === 'string' ? { layer: candidate.layer } : {}),
220
+ ...(typeof candidate.stage === 'string' ? { stage: candidate.stage } : {}),
221
+ };
222
+ throw new SagApiError(response.status, this.redact(parsed.code), this.redact(parsed.message), parsed.retryable, parsed.request_id === undefined ? undefined : this.redact(parsed.request_id), parsed.layer === undefined ? undefined : this.redact(parsed.layer), parsed.stage === undefined ? undefined : this.redact(parsed.stage));
223
+ }
224
+ return payload;
225
+ }
226
+ redact(message) {
227
+ return message.split(this.descriptor.accessToken).join('<redacted>');
228
+ }
229
+ json(method, body) {
230
+ return { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) };
231
+ }
232
+ /** Check whether the SAG API process is alive. */
233
+ health(signal) { return this.requestJson('/system/health', {}, signal); }
234
+ /** Check whether SAG storage and knowledge runtime are ready. */
235
+ ready(signal) { return this.requestJson('/system/ready', {}, signal); }
236
+ /** Read the versioned dsh integration capabilities. */
237
+ async capabilities(signal) {
238
+ return parseCapability(await this.requestJson('/system/dsh', {}, signal));
239
+ }
240
+ /** List all local knowledge sources. */
241
+ async listSources(signal) {
242
+ const value = await this.requestJson('/sources', {}, signal);
243
+ if (!Array.isArray(value))
244
+ throw new Error('dsh-sag: source list must be an array');
245
+ return value.map(parseSource);
246
+ }
247
+ /** Create one file-upload or configured SAG source. */
248
+ async createSource(request, signal) {
249
+ return parseSource(await this.requestJson('/sources', this.json('POST', {
250
+ name: request.name,
251
+ description: request.description ?? '',
252
+ connector_kind: request.connectorKind ?? 'file_upload',
253
+ config: request.config ?? {},
254
+ }), signal));
255
+ }
256
+ /** Search one or more sources using SAG's structured workspace response. */
257
+ async search(request, signal) {
258
+ const payload = await this.requestJson('/search', this.json('POST', {
259
+ query: request.query,
260
+ ...(request.sourceIds === undefined ? {} : { source_ids: request.sourceIds }),
261
+ ...(request.topK === undefined ? {} : { top_k: request.topK }),
262
+ ...(request.strategy === undefined ? {} : { strategy: request.strategy }),
263
+ save_exploration: false,
264
+ }), signal);
265
+ const root = object(payload, 'search response');
266
+ if (!Array.isArray(root.sections))
267
+ throw new Error('dsh-sag: search response sections must be an array');
268
+ const sections = root.sections.flatMap((item, index) => {
269
+ const section = object(item, `search response sections[${index}]`);
270
+ if (section.source_id === null || section.chunk_id === null)
271
+ return [];
272
+ return [{
273
+ sourceId: string(section.source_id, `search section ${index} source_id`),
274
+ sourceName: section.source_name === null || section.source_name === undefined ? null : string(section.source_name, `search section ${index} source_name`),
275
+ chunkId: string(section.chunk_id, `search section ${index} chunk_id`),
276
+ heading: typeof section.heading === 'string' ? section.heading : '',
277
+ content: string(section.content, `search section ${index} content`),
278
+ score: number(section.score, `search section ${index} score`),
279
+ rank: number(section.rank, `search section ${index} rank`),
280
+ }];
281
+ });
282
+ return {
283
+ query: string(root.query, 'search response query'),
284
+ sections,
285
+ summary: typeof root.summary === 'string' ? root.summary : '',
286
+ stats: object(root.stats, 'search response stats'),
287
+ };
288
+ }
289
+ /** Read one complete chunk by its source-local identifiers. */
290
+ async readChunk(sourceId, chunkId, signal) {
291
+ const value = recordWithCamelIds(await this.requestJson(`/sources/${encodePath(sourceId)}/chunks/${encodePath(chunkId)}`, {}, signal), 'chunk');
292
+ const content = typeof value.content === 'string' ? value.content : typeof value.text === 'string' ? value.text : undefined;
293
+ return {
294
+ ...value,
295
+ sourceId: typeof value.sourceId === 'string' ? value.sourceId : sourceId,
296
+ chunkId: string(value.chunkId, 'chunk.chunk_id'),
297
+ content: string(content, 'chunk content'),
298
+ };
299
+ }
300
+ /** List documents belonging to one source. */
301
+ async listDocuments(sourceId, signal) {
302
+ const value = await this.requestJson(`/sources/${encodePath(sourceId)}/documents`, {}, signal);
303
+ if (!Array.isArray(value))
304
+ throw new Error('dsh-sag: document list must be an array');
305
+ return value.map(parseDocument);
306
+ }
307
+ /** Get one document and its current processing status. */
308
+ async getDocument(sourceId, documentId, signal) {
309
+ return parseDocument(await this.requestJson(`/sources/${encodePath(sourceId)}/documents/${encodePath(documentId)}`, {}, signal));
310
+ }
311
+ /** Upload bytes already read through dsh FS as one multipart document. */
312
+ async uploadFile(upload, signal) {
313
+ const form = new FormData();
314
+ const bytes = Uint8Array.from(upload.bytes);
315
+ form.set('file', new Blob([bytes.buffer], { type: upload.contentType }), upload.filename);
316
+ return parseDocument(await this.requestJson(`/sources/${encodePath(upload.sourceId)}/documents`, { method: 'POST', body: form }, signal));
317
+ }
318
+ /** Ingest text or messages without waiting for background parsing. */
319
+ async ingestText(ingest, signal) {
320
+ return parseDocument(await this.requestJson(`/sources/${encodePath(ingest.sourceId)}/documents/ingest`, this.json('POST', {
321
+ text: ingest.text,
322
+ ...(ingest.title === undefined ? {} : { title: ingest.title }),
323
+ ...(ingest.messages === undefined ? {} : { messages: ingest.messages }),
324
+ }), signal));
325
+ }
326
+ /** Queue reprocessing for one document. */
327
+ async reprocessDocument(sourceId, documentId, signal) {
328
+ const job = parseJob(await this.requestJson(`/sources/${encodePath(sourceId)}/documents/${encodePath(documentId)}/reprocess`, { method: 'POST' }, signal));
329
+ if (job.type !== 'reprocess_document')
330
+ throw new Error('dsh-sag: reprocess job.type must be reprocess_document');
331
+ return job;
332
+ }
333
+ /** Delete one document after the caller has obtained dsh approval. */
334
+ async deleteDocument(sourceId, documentId, signal) {
335
+ return this.requestJson(`/sources/${encodePath(sourceId)}/documents/${encodePath(documentId)}`, { method: 'DELETE' }, signal);
336
+ }
337
+ }
338
+ //# sourceMappingURL=api-client.js.map
@@ -0,0 +1,43 @@
1
+ import { type CreateSourceRequest, type SagApiClient, type SagChunk, type SagDocument, type SagFileUpload, type SagReprocessJob, type SagSearchRequest, type SagSource, type SagTextIngest } from './api-client.js';
2
+ import type { SagCapabilityDescriptor } from '../connection/types.js';
3
+ /** One search section with an opaque read reference for the model-facing tool. */
4
+ export interface SagEvidence {
5
+ readonly evidenceRef: string;
6
+ readonly sourceId: string;
7
+ readonly sourceName: string | null;
8
+ readonly chunkId: string;
9
+ readonly heading: string;
10
+ readonly content: string;
11
+ readonly score: number;
12
+ readonly rank: number;
13
+ }
14
+ /** Stable local search output independent of SAG's wire field names. */
15
+ export interface SagGatewaySearchResult {
16
+ readonly query: string;
17
+ readonly evidences: readonly SagEvidence[];
18
+ readonly summary: string;
19
+ readonly stats: Readonly<Record<string, unknown>>;
20
+ }
21
+ /** Stable operations consumed by local dsh-sag tools and diagnostics. */
22
+ export interface SagGateway {
23
+ /** Release optional transport resources owned by a custom gateway. */
24
+ close?(): void | Promise<void>;
25
+ health(signal: AbortSignal): Promise<unknown>;
26
+ ready(signal: AbortSignal): Promise<unknown>;
27
+ capabilities(signal: AbortSignal): Promise<SagCapabilityDescriptor>;
28
+ listSources(signal: AbortSignal): Promise<readonly SagSource[]>;
29
+ createSource(request: CreateSourceRequest, signal: AbortSignal): Promise<SagSource>;
30
+ search(request: SagSearchRequest, signal: AbortSignal): Promise<SagGatewaySearchResult>;
31
+ read(request: {
32
+ readonly evidenceRef: string;
33
+ }, signal: AbortSignal): Promise<SagChunk>;
34
+ listDocuments(sourceId: string, signal: AbortSignal): Promise<readonly SagDocument[]>;
35
+ getDocument(sourceId: string, documentId: string, signal: AbortSignal): Promise<SagDocument>;
36
+ uploadFile(request: SagFileUpload, signal: AbortSignal): Promise<SagDocument>;
37
+ ingestText(request: SagTextIngest, signal: AbortSignal): Promise<SagDocument>;
38
+ reprocessDocument(sourceId: string, documentId: string, signal: AbortSignal): Promise<SagReprocessJob>;
39
+ deleteDocument(sourceId: string, documentId: string, signal: AbortSignal): Promise<unknown>;
40
+ }
41
+ /** Adapt the SAG REST client to stable camel-case domain operations. */
42
+ export declare function createSagGateway(api: SagApiClient): SagGateway;
43
+ //# sourceMappingURL=gateway.d.ts.map
@@ -0,0 +1,34 @@
1
+ import { decodeLocalEvidenceRef, encodeLocalEvidenceRef, } from './api-client.js';
2
+ /** Adapt the SAG REST client to stable camel-case domain operations. */
3
+ export function createSagGateway(api) {
4
+ return {
5
+ health: signal => api.health(signal),
6
+ ready: signal => api.ready(signal),
7
+ capabilities: signal => api.capabilities(signal),
8
+ listSources: signal => api.listSources(signal),
9
+ createSource: (request, signal) => api.createSource(request, signal),
10
+ async search(request, signal) {
11
+ const result = await api.search(request, signal);
12
+ return {
13
+ query: result.query,
14
+ summary: result.summary,
15
+ stats: result.stats,
16
+ evidences: result.sections.map(section => ({
17
+ ...section,
18
+ evidenceRef: encodeLocalEvidenceRef({ v: 1, sourceId: section.sourceId, chunkId: section.chunkId }),
19
+ })),
20
+ };
21
+ },
22
+ read(request, signal) {
23
+ const ref = decodeLocalEvidenceRef(request.evidenceRef);
24
+ return api.readChunk(ref.sourceId, ref.chunkId, signal);
25
+ },
26
+ listDocuments: (sourceId, signal) => api.listDocuments(sourceId, signal),
27
+ getDocument: (sourceId, documentId, signal) => api.getDocument(sourceId, documentId, signal),
28
+ uploadFile: (request, signal) => api.uploadFile(request, signal),
29
+ ingestText: (request, signal) => api.ingestText(request, signal),
30
+ reprocessDocument: (sourceId, documentId, signal) => api.reprocessDocument(sourceId, documentId, signal),
31
+ deleteDocument: (sourceId, documentId, signal) => api.deleteDocument(sourceId, documentId, signal),
32
+ };
33
+ }
34
+ //# sourceMappingURL=gateway.js.map