@forgeax/engine-intelligence-dsh 0.0.0-dev.8d955ade1c79
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 +202 -0
- package/README.md +28 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/dsh-provider.test.d.ts +2 -0
- package/dist/__tests__/dsh-provider.test.d.ts.map +1 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +143 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +61 -0
- package/src/__tests__/dsh-provider.test.ts +314 -0
- package/src/__tests__/fixtures/fake-dsh-runtime.mjs +77 -0
- package/src/index.ts +229 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client';
|
|
2
|
+
import { IntelligenceError } from '@forgeax/engine-intelligence';
|
|
3
|
+
import { err, ok } from '@forgeax/engine-types';
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
function closeActivity(activity) {
|
|
7
|
+
if (activity.closeTask !== void 0) return activity.closeTask;
|
|
8
|
+
activity.closeTask = (async () => {
|
|
9
|
+
try {
|
|
10
|
+
await activity.harness.close();
|
|
11
|
+
} catch {
|
|
12
|
+
try {
|
|
13
|
+
await activity.harness.close();
|
|
14
|
+
} catch {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
})();
|
|
19
|
+
return activity.closeTask;
|
|
20
|
+
}
|
|
21
|
+
function textDelta(notification) {
|
|
22
|
+
if (notification.method !== "session.event") return void 0;
|
|
23
|
+
const event = notification.params.event;
|
|
24
|
+
if (typeof event !== "object" || event === null || Array.isArray(event)) return void 0;
|
|
25
|
+
if (Reflect.get(event, "type") !== "assistant/chunk") return void 0;
|
|
26
|
+
const data = Reflect.get(event, "data");
|
|
27
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) return void 0;
|
|
28
|
+
const chunk = Reflect.get(data, "chunk");
|
|
29
|
+
if (typeof chunk !== "object" || chunk === null || Array.isArray(chunk)) return void 0;
|
|
30
|
+
if (Reflect.get(chunk, "type") !== "text-delta") return void 0;
|
|
31
|
+
const text = Reflect.get(chunk, "text");
|
|
32
|
+
return typeof text === "string" ? text : void 0;
|
|
33
|
+
}
|
|
34
|
+
function defaultHarness(options) {
|
|
35
|
+
const launch = {
|
|
36
|
+
command: options.launch.command,
|
|
37
|
+
...options.launch.args === void 0 ? {} : { args: [...options.launch.args] },
|
|
38
|
+
...options.launch.env === void 0 ? {} : { env: { ...options.launch.env } },
|
|
39
|
+
...options.launch.cwd === void 0 ? {} : { cwd: options.launch.cwd },
|
|
40
|
+
...options.launch.requestTimeoutMs === void 0 ? {} : { requestTimeoutMs: options.launch.requestTimeoutMs },
|
|
41
|
+
...options.launch.shutdownTimeoutMs === void 0 ? {} : { shutdownTimeoutMs: options.launch.shutdownTimeoutMs },
|
|
42
|
+
...options.launch.disposeEofGraceMs === void 0 ? {} : { disposeEofGraceMs: options.launch.disposeEofGraceMs },
|
|
43
|
+
...options.launch.disposeGraceMs === void 0 ? {} : { disposeGraceMs: options.launch.disposeGraceMs }
|
|
44
|
+
};
|
|
45
|
+
return new DeepSeekHarness({
|
|
46
|
+
launch,
|
|
47
|
+
...options.cwd === void 0 ? {} : { cwd: options.cwd },
|
|
48
|
+
...options.provider === void 0 ? {} : { provider: options.provider },
|
|
49
|
+
...options.model === void 0 ? {} : { model: options.model },
|
|
50
|
+
...options.maxTokens === void 0 ? {} : { maxTokens: options.maxTokens }
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function createDshIntelligenceProvider(options) {
|
|
54
|
+
const activities = /* @__PURE__ */ new Map();
|
|
55
|
+
const createHarness = options.createHarness ?? defaultHarness;
|
|
56
|
+
let closed = false;
|
|
57
|
+
const provider = {
|
|
58
|
+
id: "deepseek-harness",
|
|
59
|
+
start(submission, sink) {
|
|
60
|
+
if (closed) {
|
|
61
|
+
return err(new IntelligenceError({ code: "intelligence-closed", detail: {} }));
|
|
62
|
+
}
|
|
63
|
+
let harness;
|
|
64
|
+
try {
|
|
65
|
+
harness = createHarness({
|
|
66
|
+
launch: options.launch,
|
|
67
|
+
...options.cwd === void 0 ? {} : { cwd: options.cwd },
|
|
68
|
+
...options.provider === void 0 ? {} : { provider: options.provider },
|
|
69
|
+
...options.model === void 0 ? {} : { model: options.model },
|
|
70
|
+
...options.maxTokens === void 0 ? {} : { maxTokens: options.maxTokens }
|
|
71
|
+
});
|
|
72
|
+
} catch (cause) {
|
|
73
|
+
return err(
|
|
74
|
+
new IntelligenceError({
|
|
75
|
+
code: "intelligence-provider-failed",
|
|
76
|
+
detail: { providerId: provider.id, cause }
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const activity = {
|
|
81
|
+
harness,
|
|
82
|
+
sink,
|
|
83
|
+
task: Promise.resolve(),
|
|
84
|
+
cancelRequested: false
|
|
85
|
+
};
|
|
86
|
+
activities.set(submission.id, activity);
|
|
87
|
+
activity.task = runActivity(submission, activity);
|
|
88
|
+
return ok(void 0);
|
|
89
|
+
},
|
|
90
|
+
cancel(id) {
|
|
91
|
+
const activity = activities.get(id);
|
|
92
|
+
if (activity === void 0) {
|
|
93
|
+
return err(
|
|
94
|
+
new IntelligenceError({
|
|
95
|
+
code: "intelligence-activity-not-found",
|
|
96
|
+
detail: { activityId: id }
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
activity.cancelRequested = true;
|
|
101
|
+
void closeActivity(activity);
|
|
102
|
+
return ok(void 0);
|
|
103
|
+
},
|
|
104
|
+
async close() {
|
|
105
|
+
if (closed) return;
|
|
106
|
+
closed = true;
|
|
107
|
+
const current = [...activities.values()];
|
|
108
|
+
for (const activity of current) {
|
|
109
|
+
activity.cancelRequested = true;
|
|
110
|
+
}
|
|
111
|
+
const closeTasks = current.map(closeActivity);
|
|
112
|
+
await Promise.allSettled(current.map((activity) => activity.task));
|
|
113
|
+
await Promise.allSettled(closeTasks);
|
|
114
|
+
activities.clear();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
async function runActivity(submission, activity) {
|
|
118
|
+
try {
|
|
119
|
+
const result = await activity.harness.run(submission.input, {
|
|
120
|
+
sessionId: submission.session.id,
|
|
121
|
+
onNotification(notification) {
|
|
122
|
+
if (activity.cancelRequested) return;
|
|
123
|
+
const delta = textDelta(notification);
|
|
124
|
+
if (delta !== void 0) activity.sink.text(delta);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
await closeActivity(activity);
|
|
128
|
+
if (activity.cancelRequested) activity.sink.cancelled();
|
|
129
|
+
else activity.sink.complete(result.finalResponse);
|
|
130
|
+
} catch (cause) {
|
|
131
|
+
await closeActivity(activity);
|
|
132
|
+
if (activity.cancelRequested) activity.sink.cancelled();
|
|
133
|
+
else activity.sink.fail(cause);
|
|
134
|
+
} finally {
|
|
135
|
+
activities.delete(submission.id);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return provider;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export { createDshIntelligenceProvider, textDelta as extractDshTextDelta };
|
|
142
|
+
//# sourceMappingURL=index.mjs.map
|
|
143
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["IntelligenceErrorValue"],"mappings":"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","file":"index.mjs","sourcesContent":["import {\n DeepSeekHarness,\n type HarnessClientOptions,\n type HarnessNotification,\n} from '@deepseek-ai/dsh-sdk-client';\nimport type {\n ActivityId,\n ActivitySink,\n ActivitySubmission,\n IntelligenceError,\n IntelligenceProvider,\n} from '@forgeax/engine-intelligence';\nimport { IntelligenceError as IntelligenceErrorValue } from '@forgeax/engine-intelligence';\nimport { err, ok, type Result } from '@forgeax/engine-types';\n\nexport interface DshLaunchOptions {\n readonly command: string;\n readonly args?: readonly string[];\n readonly cwd?: string;\n readonly env?: Readonly<Record<string, string | undefined>>;\n readonly requestTimeoutMs?: number;\n readonly shutdownTimeoutMs?: number;\n readonly disposeEofGraceMs?: number;\n readonly disposeGraceMs?: number;\n}\n\nexport interface DshIntelligenceProviderOptions {\n readonly launch: DshLaunchOptions;\n readonly cwd?: string;\n readonly provider?: string;\n readonly model?: string;\n readonly maxTokens?: number;\n /** Test/integration seam; production uses the pinned SDK client. */\n readonly createHarness?: (options: DshHarnessOptions) => DshHarness;\n}\n\nexport interface DshHarnessOptions {\n readonly launch: DshLaunchOptions;\n readonly cwd?: string;\n readonly provider?: string;\n readonly model?: string;\n readonly maxTokens?: number;\n}\n\nexport interface DshRunResult {\n readonly finalResponse: string;\n}\n\nexport interface DshHarness {\n run(\n input: string,\n options: {\n readonly sessionId: string;\n readonly onNotification: (notification: DshNotification) => void;\n },\n ): Promise<DshRunResult>;\n close(): Promise<void>;\n}\n\nexport interface DshNotification {\n readonly method: string;\n readonly params: Record<string, unknown>;\n}\n\ninterface DshActivity {\n readonly harness: DshHarness;\n readonly sink: ActivitySink;\n task: Promise<void>;\n cancelRequested: boolean;\n closeTask?: Promise<void>;\n}\n\nfunction closeActivity(activity: DshActivity): Promise<void> {\n if (activity.closeTask !== undefined) return activity.closeTask;\n activity.closeTask = (async () => {\n try {\n await activity.harness.close();\n } catch {\n try {\n await activity.harness.close();\n } catch {\n return;\n }\n }\n })();\n return activity.closeTask;\n}\n\nfunction textDelta(notification: DshNotification): string | undefined {\n if (notification.method !== 'session.event') return undefined;\n const event = notification.params.event;\n if (typeof event !== 'object' || event === null || Array.isArray(event)) return undefined;\n if (Reflect.get(event, 'type') !== 'assistant/chunk') return undefined;\n const data = Reflect.get(event, 'data');\n if (typeof data !== 'object' || data === null || Array.isArray(data)) return undefined;\n const chunk = Reflect.get(data, 'chunk');\n if (typeof chunk !== 'object' || chunk === null || Array.isArray(chunk)) return undefined;\n if (Reflect.get(chunk, 'type') !== 'text-delta') return undefined;\n const text = Reflect.get(chunk, 'text');\n return typeof text === 'string' ? text : undefined;\n}\n\nfunction defaultHarness(options: DshHarnessOptions): DshHarness {\n const launch = {\n command: options.launch.command,\n ...(options.launch.args === undefined ? {} : { args: [...options.launch.args] }),\n ...(options.launch.env === undefined ? {} : { env: { ...options.launch.env } }),\n ...(options.launch.cwd === undefined ? {} : { cwd: options.launch.cwd }),\n ...(options.launch.requestTimeoutMs === undefined\n ? {}\n : { requestTimeoutMs: options.launch.requestTimeoutMs }),\n ...(options.launch.shutdownTimeoutMs === undefined\n ? {}\n : { shutdownTimeoutMs: options.launch.shutdownTimeoutMs }),\n ...(options.launch.disposeEofGraceMs === undefined\n ? {}\n : { disposeEofGraceMs: options.launch.disposeEofGraceMs }),\n ...(options.launch.disposeGraceMs === undefined\n ? {}\n : { disposeGraceMs: options.launch.disposeGraceMs }),\n } satisfies HarnessClientOptions;\n return new DeepSeekHarness({\n launch,\n ...(options.cwd === undefined ? {} : { cwd: options.cwd }),\n ...(options.provider === undefined ? {} : { provider: options.provider }),\n ...(options.model === undefined ? {} : { model: options.model }),\n ...(options.maxTokens === undefined ? {} : { maxTokens: options.maxTokens }),\n });\n}\n\n/**\n * Create a Host-only DSH provider. Each Activity owns one runtime process.\n * DSH 0.1 has process close but no wire-level turn cancel; process-per-Activity\n * preserves honest cancellation without terminating unrelated work.\n */\nexport function createDshIntelligenceProvider(\n options: DshIntelligenceProviderOptions,\n): IntelligenceProvider {\n const activities = new Map<ActivityId, DshActivity>();\n const createHarness = options.createHarness ?? defaultHarness;\n let closed = false;\n\n const provider: IntelligenceProvider = {\n id: 'deepseek-harness',\n start(submission, sink): Result<void, IntelligenceError> {\n if (closed) {\n return err(new IntelligenceErrorValue({ code: 'intelligence-closed', detail: {} }));\n }\n let harness: DshHarness;\n try {\n harness = createHarness({\n launch: options.launch,\n ...(options.cwd === undefined ? {} : { cwd: options.cwd }),\n ...(options.provider === undefined ? {} : { provider: options.provider }),\n ...(options.model === undefined ? {} : { model: options.model }),\n ...(options.maxTokens === undefined ? {} : { maxTokens: options.maxTokens }),\n });\n } catch (cause) {\n return err(\n new IntelligenceErrorValue({\n code: 'intelligence-provider-failed',\n detail: { providerId: provider.id, cause },\n }),\n );\n }\n const activity: DshActivity = {\n harness,\n sink,\n task: Promise.resolve(),\n cancelRequested: false,\n };\n activities.set(submission.id, activity);\n activity.task = runActivity(submission, activity);\n return ok(undefined);\n },\n cancel(id): Result<void, IntelligenceError> {\n const activity = activities.get(id);\n if (activity === undefined) {\n return err(\n new IntelligenceErrorValue({\n code: 'intelligence-activity-not-found',\n detail: { activityId: id },\n }),\n );\n }\n activity.cancelRequested = true;\n void closeActivity(activity);\n return ok(undefined);\n },\n async close(): Promise<void> {\n if (closed) return;\n closed = true;\n const current = [...activities.values()];\n for (const activity of current) {\n activity.cancelRequested = true;\n }\n const closeTasks = current.map(closeActivity);\n await Promise.allSettled(current.map((activity) => activity.task));\n await Promise.allSettled(closeTasks);\n activities.clear();\n },\n };\n\n async function runActivity(submission: ActivitySubmission, activity: DshActivity): Promise<void> {\n try {\n const result = await activity.harness.run(submission.input, {\n sessionId: submission.session.id,\n onNotification(notification: HarnessNotification) {\n if (activity.cancelRequested) return;\n const delta = textDelta(notification);\n if (delta !== undefined) activity.sink.text(delta);\n },\n });\n await closeActivity(activity);\n if (activity.cancelRequested) activity.sink.cancelled();\n else activity.sink.complete(result.finalResponse);\n } catch (cause) {\n await closeActivity(activity);\n if (activity.cancelRequested) activity.sink.cancelled();\n else activity.sink.fail(cause);\n } finally {\n activities.delete(submission.id);\n }\n }\n\n return provider;\n}\n\nexport { textDelta as extractDshTextDelta };\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forgeax/engine-intelligence-dsh",
|
|
3
|
+
"version": "0.0.0-dev.8d955ade1c79",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"description": "Host-side DeepSeek Harness provider for ForgeaX intelligence Activities.",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"main": "./dist/index.mjs",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"src",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@deepseek-ai/dsh-sdk-client": "0.1.0-rc.6",
|
|
25
|
+
"@forgeax/engine-intelligence": "0.0.0-dev.8d955ade1c79",
|
|
26
|
+
"@forgeax/engine-types": "0.0.0-dev.8d955ade1c79"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@forgeax/engine-intelligence-fake": "0.0.0-dev.8d955ade1c79",
|
|
30
|
+
"@types/node": "^20.14.0"
|
|
31
|
+
},
|
|
32
|
+
"forgeax": {
|
|
33
|
+
"metrics": {
|
|
34
|
+
"bundle-size": {
|
|
35
|
+
"enabled": true,
|
|
36
|
+
"path": "dist/index.mjs",
|
|
37
|
+
"compression": "gzip"
|
|
38
|
+
},
|
|
39
|
+
"fps": {
|
|
40
|
+
"enabled": false,
|
|
41
|
+
"reason": "Host-side subprocess adapter never runs in the frame loop"
|
|
42
|
+
},
|
|
43
|
+
"bench": {
|
|
44
|
+
"enabled": false,
|
|
45
|
+
"reason": "provider latency is external and frame polling remains in engine-intelligence"
|
|
46
|
+
},
|
|
47
|
+
"gate": {
|
|
48
|
+
"enabled": false,
|
|
49
|
+
"reason": "adapter lifecycle is gated by package integration tests"
|
|
50
|
+
},
|
|
51
|
+
"spike-report": {
|
|
52
|
+
"enabled": false,
|
|
53
|
+
"reason": "not a spike package"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsup",
|
|
59
|
+
"test": "vitest run"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { mkdtemp, readFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import {
|
|
6
|
+
type ActivityEvent,
|
|
7
|
+
createIntelligenceRuntime,
|
|
8
|
+
type IntelligenceService,
|
|
9
|
+
type SessionRef,
|
|
10
|
+
} from '@forgeax/engine-intelligence';
|
|
11
|
+
import { createFakeIntelligenceProvider } from '@forgeax/engine-intelligence-fake';
|
|
12
|
+
import { describe, expect, it } from 'vitest';
|
|
13
|
+
import { createDshIntelligenceProvider, type DshHarness, extractDshTextDelta } from '../index';
|
|
14
|
+
|
|
15
|
+
const fixture = join(dirname(fileURLToPath(import.meta.url)), 'fixtures/fake-dsh-runtime.mjs');
|
|
16
|
+
|
|
17
|
+
function turn(): Promise<void> {
|
|
18
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface DialogueView {
|
|
22
|
+
text: string;
|
|
23
|
+
terminal: boolean;
|
|
24
|
+
session?: SessionRef;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function updateDialogue(service: IntelligenceService, view: DialogueView): void {
|
|
28
|
+
for (const event of service.poll()) {
|
|
29
|
+
if (event.type === 'text-delta') view.text += event.text;
|
|
30
|
+
else if (event.type === 'completed') {
|
|
31
|
+
view.terminal = true;
|
|
32
|
+
view.session = event.session;
|
|
33
|
+
} else if (event.type === 'failed' || event.type === 'cancelled') {
|
|
34
|
+
view.terminal = true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function waitForTerminal(service: IntelligenceService, view: DialogueView): Promise<void> {
|
|
40
|
+
for (let attempt = 0; attempt < 200 && !view.terminal; attempt += 1) {
|
|
41
|
+
updateDialogue(service, view);
|
|
42
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
43
|
+
}
|
|
44
|
+
expect(view.terminal).toBe(true);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('DeepSeek Harness intelligence provider', () => {
|
|
48
|
+
it('projects only text deltas from DSH notifications', () => {
|
|
49
|
+
expect(
|
|
50
|
+
extractDshTextDelta({
|
|
51
|
+
method: 'session.event',
|
|
52
|
+
params: {
|
|
53
|
+
event: {
|
|
54
|
+
type: 'assistant/chunk',
|
|
55
|
+
data: { chunk: { type: 'text-delta', text: 'token' } },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
).toBe('token');
|
|
60
|
+
expect(extractDshTextDelta({ method: 'session.status', params: {} })).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('lets one dialogue consumer swap fake and DSH providers', async () => {
|
|
64
|
+
const fake = createFakeIntelligenceProvider({
|
|
65
|
+
script: () => ({ deltas: ['same'], output: 'same' }),
|
|
66
|
+
});
|
|
67
|
+
const fakeService = createIntelligenceRuntime(fake);
|
|
68
|
+
const fakeView: DialogueView = { text: '', terminal: false };
|
|
69
|
+
fakeService.submit({ input: 'prompt' });
|
|
70
|
+
fake.advance();
|
|
71
|
+
fake.advance();
|
|
72
|
+
updateDialogue(fakeService, fakeView);
|
|
73
|
+
|
|
74
|
+
let closeCount = 0;
|
|
75
|
+
const harness: DshHarness = {
|
|
76
|
+
async run(_input, options) {
|
|
77
|
+
options.onNotification({
|
|
78
|
+
method: 'session.event',
|
|
79
|
+
params: {
|
|
80
|
+
event: {
|
|
81
|
+
type: 'assistant/chunk',
|
|
82
|
+
data: { chunk: { type: 'text-delta', text: 'same' } },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
return { finalResponse: 'same' };
|
|
87
|
+
},
|
|
88
|
+
async close() {
|
|
89
|
+
closeCount += 1;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
const dshService = createIntelligenceRuntime(
|
|
93
|
+
createDshIntelligenceProvider({
|
|
94
|
+
launch: { command: 'unused' },
|
|
95
|
+
createHarness: () => harness,
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
const dshView: DialogueView = { text: '', terminal: false };
|
|
99
|
+
dshService.submit({ input: 'prompt' });
|
|
100
|
+
await turn();
|
|
101
|
+
updateDialogue(dshService, dshView);
|
|
102
|
+
|
|
103
|
+
expect(fakeView.text).toBe('same');
|
|
104
|
+
expect(dshView.text).toBe('same');
|
|
105
|
+
expect(fakeView.terminal).toBe(true);
|
|
106
|
+
expect(dshView.terminal).toBe(true);
|
|
107
|
+
expect(closeCount).toBeGreaterThan(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('streams through the real SDK transport and reaps the runtime on completion', async () => {
|
|
111
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-intelligence-dsh-'));
|
|
112
|
+
const lifecycle = join(root, 'lifecycle.log');
|
|
113
|
+
const provider = createDshIntelligenceProvider({
|
|
114
|
+
launch: {
|
|
115
|
+
command: process.execPath,
|
|
116
|
+
args: [fixture],
|
|
117
|
+
env: { DSH_FAKE_LIFECYCLE: lifecycle },
|
|
118
|
+
requestTimeoutMs: 2_000,
|
|
119
|
+
shutdownTimeoutMs: 500,
|
|
120
|
+
disposeEofGraceMs: 500,
|
|
121
|
+
disposeGraceMs: 500,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
const service = createIntelligenceRuntime(provider);
|
|
125
|
+
const view: DialogueView = { text: '', terminal: false };
|
|
126
|
+
const started = service.submit({ input: 'complete' });
|
|
127
|
+
if (!started.ok) throw started.error;
|
|
128
|
+
await waitForTerminal(service, view);
|
|
129
|
+
await service.close();
|
|
130
|
+
|
|
131
|
+
expect(view.text).toBe('hello');
|
|
132
|
+
expect(view.session).toEqual(started.value.session);
|
|
133
|
+
const lines = (await readFile(lifecycle, 'utf8')).trim().split('\n');
|
|
134
|
+
expect(lines).toHaveLength(2);
|
|
135
|
+
expect(lines[0]?.replace('started:', '')).toBe(lines[1]?.replace('closed:', ''));
|
|
136
|
+
}, 10_000);
|
|
137
|
+
|
|
138
|
+
it('cancels one Activity by closing only its owned DSH runtime', async () => {
|
|
139
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-intelligence-dsh-cancel-'));
|
|
140
|
+
const lifecycle = join(root, 'lifecycle.log');
|
|
141
|
+
const service = createIntelligenceRuntime(
|
|
142
|
+
createDshIntelligenceProvider({
|
|
143
|
+
launch: {
|
|
144
|
+
command: process.execPath,
|
|
145
|
+
args: [fixture],
|
|
146
|
+
env: { DSH_FAKE_LIFECYCLE: lifecycle },
|
|
147
|
+
requestTimeoutMs: 2_000,
|
|
148
|
+
shutdownTimeoutMs: 500,
|
|
149
|
+
disposeEofGraceMs: 500,
|
|
150
|
+
disposeGraceMs: 500,
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
const started = service.submit({ input: 'hang' });
|
|
155
|
+
if (!started.ok) throw started.error;
|
|
156
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
157
|
+
expect(service.cancel(started.value.id).ok).toBe(true);
|
|
158
|
+
const view: DialogueView = { text: '', terminal: false };
|
|
159
|
+
await waitForTerminal(service, view);
|
|
160
|
+
await service.close();
|
|
161
|
+
|
|
162
|
+
const lines = (await readFile(lifecycle, 'utf8')).trim().split('\n');
|
|
163
|
+
expect(lines).toHaveLength(2);
|
|
164
|
+
expect(lines[0]?.replace('started:', '')).toBe(lines[1]?.replace('closed:', ''));
|
|
165
|
+
}, 10_000);
|
|
166
|
+
|
|
167
|
+
it('contains a refused cancellation close without affecting a sibling', async () => {
|
|
168
|
+
const harnesses: Array<{
|
|
169
|
+
active: boolean;
|
|
170
|
+
closeCalls: number;
|
|
171
|
+
closed: boolean;
|
|
172
|
+
input?: string;
|
|
173
|
+
rejectRun?: (cause: Error) => void;
|
|
174
|
+
resolveClose?: () => void;
|
|
175
|
+
resolveRun?: () => void;
|
|
176
|
+
}> = [];
|
|
177
|
+
const provider = createDshIntelligenceProvider({
|
|
178
|
+
launch: {
|
|
179
|
+
command: process.execPath,
|
|
180
|
+
args: [],
|
|
181
|
+
},
|
|
182
|
+
createHarness: () => {
|
|
183
|
+
const state: (typeof harnesses)[number] = {
|
|
184
|
+
active: false,
|
|
185
|
+
closeCalls: 0,
|
|
186
|
+
closed: false,
|
|
187
|
+
};
|
|
188
|
+
harnesses.push(state);
|
|
189
|
+
return {
|
|
190
|
+
run: (input: string) => {
|
|
191
|
+
state.input = input;
|
|
192
|
+
state.active = true;
|
|
193
|
+
if (input === 'activity-a') {
|
|
194
|
+
return new Promise<never>((_resolve, reject) => {
|
|
195
|
+
state.rejectRun = reject;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return new Promise((resolve) => {
|
|
199
|
+
state.resolveRun = () => resolve({ finalResponse: 'activity-b-complete' });
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
close: async () => {
|
|
203
|
+
state.closeCalls += 1;
|
|
204
|
+
if (state.input === 'activity-a' && state.closeCalls === 1) {
|
|
205
|
+
state.active = false;
|
|
206
|
+
state.rejectRun?.(new Error('activity A close refused'));
|
|
207
|
+
throw new Error('activity A close refused');
|
|
208
|
+
}
|
|
209
|
+
if (state.input === 'activity-a' && state.closeCalls === 2) {
|
|
210
|
+
await new Promise<void>((resolve) => {
|
|
211
|
+
state.resolveClose = () => {
|
|
212
|
+
state.closed = true;
|
|
213
|
+
resolve();
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
state.active = false;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
state.active = false;
|
|
220
|
+
state.closed = true;
|
|
221
|
+
},
|
|
222
|
+
} satisfies DshHarness;
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
const service = createIntelligenceRuntime(provider);
|
|
226
|
+
const events: ActivityEvent[] = [];
|
|
227
|
+
const unhandledRejections: unknown[] = [];
|
|
228
|
+
const onUnhandledRejection = (reason: unknown) => {
|
|
229
|
+
unhandledRejections.push(reason);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const activityA = service.submit({ input: 'activity-a' });
|
|
233
|
+
const activityB = service.submit({ input: 'activity-b' });
|
|
234
|
+
expect(activityA.ok).toBe(true);
|
|
235
|
+
expect(activityB.ok).toBe(true);
|
|
236
|
+
if (!activityA.ok || !activityB.ok) {
|
|
237
|
+
throw new Error('expected both activities to be accepted');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await turn();
|
|
241
|
+
expect(harnesses).toHaveLength(2);
|
|
242
|
+
expect(harnesses[0]).toMatchObject({ active: true, closeCalls: 0 });
|
|
243
|
+
expect(harnesses[1]).toMatchObject({ active: true, closeCalls: 0 });
|
|
244
|
+
|
|
245
|
+
process.on('unhandledRejection', onUnhandledRejection);
|
|
246
|
+
try {
|
|
247
|
+
const cancelled = service.cancel(activityA.value.id);
|
|
248
|
+
expect(cancelled.ok).toBe(true);
|
|
249
|
+
|
|
250
|
+
expect(harnesses[1]).toMatchObject({ active: true, closeCalls: 0 });
|
|
251
|
+
harnesses[1]?.resolveRun?.();
|
|
252
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
253
|
+
events.push(...service.poll());
|
|
254
|
+
if (
|
|
255
|
+
events.some(
|
|
256
|
+
(event) => event.activityId === activityB.value.id && event.type === 'completed',
|
|
257
|
+
)
|
|
258
|
+
) {
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
await turn();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let providerClosed = false;
|
|
265
|
+
const providerCloseTask = provider.close().then(() => {
|
|
266
|
+
providerClosed = true;
|
|
267
|
+
});
|
|
268
|
+
await turn();
|
|
269
|
+
expect(providerClosed).toBe(false);
|
|
270
|
+
harnesses[0]?.resolveClose?.();
|
|
271
|
+
await providerCloseTask;
|
|
272
|
+
events.push(...service.poll());
|
|
273
|
+
await provider.close();
|
|
274
|
+
await service.close();
|
|
275
|
+
await service.close();
|
|
276
|
+
await turn();
|
|
277
|
+
} finally {
|
|
278
|
+
process.off('unhandledRejection', onUnhandledRejection);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const activityATerminals = events.filter(
|
|
282
|
+
(event) =>
|
|
283
|
+
event.activityId === activityA.value.id &&
|
|
284
|
+
(event.type === 'cancelled' || event.type === 'completed' || event.type === 'failed'),
|
|
285
|
+
);
|
|
286
|
+
const activityBTerminals = events.filter(
|
|
287
|
+
(event) =>
|
|
288
|
+
event.activityId === activityB.value.id &&
|
|
289
|
+
(event.type === 'cancelled' || event.type === 'completed' || event.type === 'failed'),
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
expect(activityATerminals).toHaveLength(1);
|
|
293
|
+
expect(activityATerminals[0]?.type).toBe('cancelled');
|
|
294
|
+
expect(activityBTerminals).toEqual([
|
|
295
|
+
expect.objectContaining({
|
|
296
|
+
activityId: activityB.value.id,
|
|
297
|
+
type: 'completed',
|
|
298
|
+
output: 'activity-b-complete',
|
|
299
|
+
}),
|
|
300
|
+
]);
|
|
301
|
+
expect(unhandledRejections).toEqual([]);
|
|
302
|
+
expect(harnesses).toHaveLength(2);
|
|
303
|
+
expect(harnesses[0]).toMatchObject({
|
|
304
|
+
closeCalls: 2,
|
|
305
|
+
closed: true,
|
|
306
|
+
active: false,
|
|
307
|
+
});
|
|
308
|
+
expect(harnesses[1]).toMatchObject({
|
|
309
|
+
closeCalls: 1,
|
|
310
|
+
closed: true,
|
|
311
|
+
active: false,
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { appendFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
const lifecyclePath = process.env.DSH_FAKE_LIFECYCLE;
|
|
4
|
+
if (lifecyclePath) appendFileSync(lifecyclePath, `started:${process.pid}\n`);
|
|
5
|
+
process.on('exit', () => {
|
|
6
|
+
if (lifecyclePath) appendFileSync(lifecyclePath, `closed:${process.pid}\n`);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
process.stdin.setEncoding('utf8');
|
|
10
|
+
let buffered = '';
|
|
11
|
+
|
|
12
|
+
function write(message) {
|
|
13
|
+
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function notify(method, params) {
|
|
17
|
+
write({ jsonrpc: '2.0', method, params });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function handle(request) {
|
|
21
|
+
if (request.method === 'initialize') {
|
|
22
|
+
write({
|
|
23
|
+
jsonrpc: '2.0',
|
|
24
|
+
id: request.id,
|
|
25
|
+
result: { serverInfo: { name: 'forgeax-fake-dsh-runtime', version: '0.1.0' } },
|
|
26
|
+
});
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (request.method === 'session/prompt') {
|
|
30
|
+
const sessionId = request.params.sessionId;
|
|
31
|
+
const messageId = `message-${request.id}`;
|
|
32
|
+
write({ jsonrpc: '2.0', id: request.id, result: { messageId } });
|
|
33
|
+
queueMicrotask(() => {
|
|
34
|
+
notify('session.event', {
|
|
35
|
+
sessionId,
|
|
36
|
+
event: { type: 'agent/inbox/spliced', data: { inserted: [{ id: messageId }] } },
|
|
37
|
+
});
|
|
38
|
+
notify('session.event', {
|
|
39
|
+
sessionId,
|
|
40
|
+
event: {
|
|
41
|
+
type: 'assistant/chunk',
|
|
42
|
+
data: { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'hello' } },
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
const text = request.params.contentBlocks?.[0]?.text;
|
|
46
|
+
if (text === 'hang') return;
|
|
47
|
+
notify('session.event', {
|
|
48
|
+
sessionId,
|
|
49
|
+
event: {
|
|
50
|
+
type: 'assistant/message',
|
|
51
|
+
data: {
|
|
52
|
+
turn: 1,
|
|
53
|
+
step: 1,
|
|
54
|
+
message: { role: 'assistant', content: [{ type: 'text', text: 'hello' }] },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
notify('session.status', { sessionId, status: 'idle' });
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (request.method === 'shutdown') {
|
|
63
|
+
write({ jsonrpc: '2.0', id: request.id, result: {} });
|
|
64
|
+
setImmediate(() => process.exit(0));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
process.stdin.on('data', (chunk) => {
|
|
69
|
+
buffered += chunk;
|
|
70
|
+
while (true) {
|
|
71
|
+
const newline = buffered.indexOf('\n');
|
|
72
|
+
if (newline < 0) break;
|
|
73
|
+
const line = buffered.slice(0, newline);
|
|
74
|
+
buffered = buffered.slice(newline + 1);
|
|
75
|
+
if (line.trim()) handle(JSON.parse(line));
|
|
76
|
+
}
|
|
77
|
+
});
|