runtimeuse 0.2.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/.env.example +4 -0
- package/README.md +222 -0
- package/dist/agent-handler.d.ts +26 -0
- package/dist/agent-handler.d.ts.map +1 -0
- package/dist/agent-handler.js +2 -0
- package/dist/agent-handler.js.map +1 -0
- package/dist/artifact-manager.d.ts +27 -0
- package/dist/artifact-manager.d.ts.map +1 -0
- package/dist/artifact-manager.js +125 -0
- package/dist/artifact-manager.js.map +1 -0
- package/dist/artifact-manager.test.d.ts +2 -0
- package/dist/artifact-manager.test.d.ts.map +1 -0
- package/dist/artifact-manager.test.js +251 -0
- package/dist/artifact-manager.test.js.map +1 -0
- package/dist/claude-handler.d.ts +3 -0
- package/dist/claude-handler.d.ts.map +1 -0
- package/dist/claude-handler.js +76 -0
- package/dist/claude-handler.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +87 -0
- package/dist/cli.js.map +1 -0
- package/dist/command-handler.d.ts +22 -0
- package/dist/command-handler.d.ts.map +1 -0
- package/dist/command-handler.js +75 -0
- package/dist/command-handler.js.map +1 -0
- package/dist/command-handler.test.d.ts +2 -0
- package/dist/command-handler.test.d.ts.map +1 -0
- package/dist/command-handler.test.js +267 -0
- package/dist/command-handler.test.js.map +1 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +13 -0
- package/dist/constants.js.map +1 -0
- package/dist/default-handler.d.ts +3 -0
- package/dist/default-handler.d.ts.map +1 -0
- package/dist/default-handler.js +76 -0
- package/dist/default-handler.js.map +1 -0
- package/dist/download-handler.d.ts +8 -0
- package/dist/download-handler.d.ts.map +1 -0
- package/dist/download-handler.js +36 -0
- package/dist/download-handler.js.map +1 -0
- package/dist/download-handler.test.d.ts +2 -0
- package/dist/download-handler.test.d.ts.map +1 -0
- package/dist/download-handler.test.js +123 -0
- package/dist/download-handler.test.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +8 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +14 -0
- package/dist/logger.js.map +1 -0
- package/dist/openai-handler.d.ts +3 -0
- package/dist/openai-handler.d.ts.map +1 -0
- package/dist/openai-handler.js +86 -0
- package/dist/openai-handler.js.map +1 -0
- package/dist/server.d.ts +21 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +52 -0
- package/dist/server.js.map +1 -0
- package/dist/session.d.ts +29 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +244 -0
- package/dist/session.js.map +1 -0
- package/dist/session.test.d.ts +2 -0
- package/dist/session.test.d.ts.map +1 -0
- package/dist/session.test.js +339 -0
- package/dist/session.test.js.map +1 -0
- package/dist/storage.d.ts +3 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +21 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/upload-tracker.d.ts +10 -0
- package/dist/upload-tracker.d.ts.map +1 -0
- package/dist/upload-tracker.js +27 -0
- package/dist/upload-tracker.js.map +1 -0
- package/dist/upload-tracker.test.d.ts +2 -0
- package/dist/upload-tracker.test.d.ts.map +1 -0
- package/dist/upload-tracker.test.js +89 -0
- package/dist/upload-tracker.test.js.map +1 -0
- package/dist/utils.d.ts +7 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +32 -0
- package/dist/utils.js.map +1 -0
- package/dist/utils.test.d.ts +2 -0
- package/dist/utils.test.d.ts.map +1 -0
- package/dist/utils.test.js +92 -0
- package/dist/utils.test.js.map +1 -0
- package/package.json +40 -0
- package/scripts/dev-publish.sh +45 -0
- package/src/agent-handler.ts +26 -0
- package/src/artifact-manager.test.ts +320 -0
- package/src/artifact-manager.ts +170 -0
- package/src/claude-handler.ts +95 -0
- package/src/cli.ts +107 -0
- package/src/command-handler.test.ts +507 -0
- package/src/command-handler.ts +102 -0
- package/src/constants.ts +12 -0
- package/src/download-handler.test.ts +183 -0
- package/src/download-handler.ts +45 -0
- package/src/index.ts +59 -0
- package/src/logger.ts +20 -0
- package/src/openai-handler.ts +120 -0
- package/src/server.ts +68 -0
- package/src/session.test.ts +448 -0
- package/src/session.ts +319 -0
- package/src/storage.ts +28 -0
- package/src/types.ts +101 -0
- package/src/upload-tracker.test.ts +112 -0
- package/src/upload-tracker.ts +30 -0
- package/src/utils.test.ts +120 -0
- package/src/utils.ts +35 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("fs", async () => {
|
|
4
|
+
const actual = await vi.importActual<typeof import("fs")>("fs");
|
|
5
|
+
return {
|
|
6
|
+
...actual,
|
|
7
|
+
default: {
|
|
8
|
+
...actual,
|
|
9
|
+
existsSync: vi.fn(() => true),
|
|
10
|
+
mkdirSync: vi.fn(),
|
|
11
|
+
createWriteStream: vi.fn(() => ({
|
|
12
|
+
on: vi.fn(),
|
|
13
|
+
write: vi.fn(),
|
|
14
|
+
end: vi.fn(),
|
|
15
|
+
close: vi.fn(),
|
|
16
|
+
})),
|
|
17
|
+
unlinkSync: vi.fn(),
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
vi.mock("child_process", () => ({
|
|
23
|
+
execFileSync: vi.fn(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock("node:stream/promises", () => ({
|
|
27
|
+
pipeline: vi.fn().mockResolvedValue(undefined),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock("node:stream", () => ({
|
|
31
|
+
Readable: { fromWeb: vi.fn().mockReturnValue("mock-stream") },
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
35
|
+
ok: true,
|
|
36
|
+
body: "mock-body",
|
|
37
|
+
status: 200,
|
|
38
|
+
statusText: "OK",
|
|
39
|
+
});
|
|
40
|
+
vi.stubGlobal("fetch", mockFetch);
|
|
41
|
+
|
|
42
|
+
import fs from "fs";
|
|
43
|
+
import { execFileSync } from "child_process";
|
|
44
|
+
import DownloadHandler from "./download-handler.js";
|
|
45
|
+
import { pipeline } from "node:stream/promises";
|
|
46
|
+
import type { Logger } from "./logger.js";
|
|
47
|
+
|
|
48
|
+
const mockLogger: Logger = {
|
|
49
|
+
log: vi.fn(),
|
|
50
|
+
error: vi.fn(),
|
|
51
|
+
debug: vi.fn(),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
describe("DownloadHandler", () => {
|
|
55
|
+
let handler: DownloadHandler;
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
vi.clearAllMocks();
|
|
59
|
+
handler = new DownloadHandler(mockLogger);
|
|
60
|
+
mockFetch.mockResolvedValue({
|
|
61
|
+
ok: true,
|
|
62
|
+
body: "mock-body",
|
|
63
|
+
status: 200,
|
|
64
|
+
statusText: "OK",
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("directory creation", () => {
|
|
69
|
+
it("creates working directory when it does not exist", async () => {
|
|
70
|
+
vi.mocked(fs.existsSync).mockReturnValueOnce(false);
|
|
71
|
+
|
|
72
|
+
await handler.download(
|
|
73
|
+
"https://example.com/files/test.tar.gz",
|
|
74
|
+
"/tmp/workdir",
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(fs.mkdirSync).toHaveBeenCalledWith("/tmp/workdir", {
|
|
78
|
+
recursive: true,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("skips directory creation when working directory exists", async () => {
|
|
83
|
+
vi.mocked(fs.existsSync).mockReturnValueOnce(true);
|
|
84
|
+
|
|
85
|
+
await handler.download(
|
|
86
|
+
"https://example.com/files/test.tar.gz",
|
|
87
|
+
"/tmp/workdir",
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(fs.mkdirSync).not.toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("file download", () => {
|
|
95
|
+
it("downloads a file via fetch to the working directory", async () => {
|
|
96
|
+
await handler.download(
|
|
97
|
+
"https://example.com/files/script.sh",
|
|
98
|
+
"/tmp/workdir",
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
102
|
+
"https://example.com/files/script.sh",
|
|
103
|
+
);
|
|
104
|
+
expect(pipeline).toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("throws on non-ok response", async () => {
|
|
108
|
+
mockFetch.mockResolvedValueOnce({
|
|
109
|
+
ok: false,
|
|
110
|
+
status: 404,
|
|
111
|
+
statusText: "Not Found",
|
|
112
|
+
body: null,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await expect(
|
|
116
|
+
handler.download("https://example.com/files/missing.sh", "/tmp/workdir"),
|
|
117
|
+
).rejects.toThrow("Download failed: 404 Not Found");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("throws when response has no body", async () => {
|
|
121
|
+
mockFetch.mockResolvedValueOnce({
|
|
122
|
+
ok: true,
|
|
123
|
+
status: 200,
|
|
124
|
+
statusText: "OK",
|
|
125
|
+
body: null,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await expect(
|
|
129
|
+
handler.download("https://example.com/files/empty.sh", "/tmp/workdir"),
|
|
130
|
+
).rejects.toThrow("Download failed: no response body");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("zip handling", () => {
|
|
135
|
+
it("unzips .zip files using execFileSync and removes the archive", async () => {
|
|
136
|
+
await handler.download(
|
|
137
|
+
"https://example.com/files/tests.zip",
|
|
138
|
+
"/tmp/workdir",
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
expect(execFileSync).toHaveBeenCalledWith("unzip", [
|
|
142
|
+
"-o",
|
|
143
|
+
"/tmp/workdir/tests.zip",
|
|
144
|
+
"-d",
|
|
145
|
+
"/tmp/workdir",
|
|
146
|
+
]);
|
|
147
|
+
expect(fs.unlinkSync).toHaveBeenCalledWith("/tmp/workdir/tests.zip");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("does not unzip non-zip files", async () => {
|
|
151
|
+
await handler.download(
|
|
152
|
+
"https://example.com/files/data.tar.gz",
|
|
153
|
+
"/tmp/workdir",
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(execFileSync).not.toHaveBeenCalled();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("logging", () => {
|
|
161
|
+
it("logs the download operation", async () => {
|
|
162
|
+
await handler.download(
|
|
163
|
+
"https://example.com/files/test.sh",
|
|
164
|
+
"/tmp/workdir",
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
expect(mockLogger.log).toHaveBeenCalledWith(
|
|
168
|
+
expect.stringContaining("Downloading file test.sh"),
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("logs unzip operation for zip files", async () => {
|
|
173
|
+
await handler.download(
|
|
174
|
+
"https://example.com/files/archive.zip",
|
|
175
|
+
"/tmp/workdir",
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(mockLogger.log).toHaveBeenCalledWith(
|
|
179
|
+
expect.stringContaining("Unzipping file archive.zip"),
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { execFileSync } from "child_process";
|
|
4
|
+
import { pipeline } from "node:stream/promises";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { Logger } from "./logger.js";
|
|
7
|
+
|
|
8
|
+
class DownloadHandler {
|
|
9
|
+
private readonly logger: Logger;
|
|
10
|
+
constructor(logger: Logger) {
|
|
11
|
+
this.logger = logger;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async download(downloadUrl: string, workingDir: string): Promise<void> {
|
|
15
|
+
if (!fs.existsSync(workingDir)) {
|
|
16
|
+
fs.mkdirSync(workingDir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
const fileUrl = new URL(downloadUrl);
|
|
19
|
+
const filename = decodeURIComponent(path.basename(fileUrl.pathname));
|
|
20
|
+
const filepath = path.join(workingDir, filename);
|
|
21
|
+
|
|
22
|
+
this.logger.log(`Downloading file ${filename} to ${filepath}`);
|
|
23
|
+
|
|
24
|
+
const response = await fetch(downloadUrl);
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`Download failed: ${response.status} ${response.statusText}`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
if (!response.body) {
|
|
31
|
+
throw new Error("Download failed: no response body");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const fileStream = fs.createWriteStream(filepath);
|
|
35
|
+
await pipeline(Readable.fromWeb(response.body as any), fileStream);
|
|
36
|
+
|
|
37
|
+
if (path.extname(filepath) === ".zip") {
|
|
38
|
+
this.logger.log(`Unzipping file ${filename}`);
|
|
39
|
+
execFileSync("unzip", ["-o", filepath, "-d", workingDir]);
|
|
40
|
+
fs.unlinkSync(filepath);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default DownloadHandler;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Core abstractions
|
|
2
|
+
export type {
|
|
3
|
+
AgentHandler,
|
|
4
|
+
AgentInvocation,
|
|
5
|
+
AgentResult,
|
|
6
|
+
MessageSender,
|
|
7
|
+
} from "./agent-handler.js";
|
|
8
|
+
|
|
9
|
+
// Built-in handlers
|
|
10
|
+
export { openaiHandler } from "./openai-handler.js";
|
|
11
|
+
export { claudeHandler } from "./claude-handler.js";
|
|
12
|
+
|
|
13
|
+
// Server
|
|
14
|
+
export { RuntimeUseServer } from "./server.js";
|
|
15
|
+
export type { RuntimeUseServerConfig } from "./server.js";
|
|
16
|
+
|
|
17
|
+
// Session
|
|
18
|
+
export { WebSocketSession } from "./session.js";
|
|
19
|
+
export type { SessionConfig } from "./session.js";
|
|
20
|
+
|
|
21
|
+
// Artifact management
|
|
22
|
+
export { ArtifactManager } from "./artifact-manager.js";
|
|
23
|
+
export type { ArtifactManagerConfig } from "./artifact-manager.js";
|
|
24
|
+
|
|
25
|
+
// Upload
|
|
26
|
+
export { UploadTracker } from "./upload-tracker.js";
|
|
27
|
+
export { uploadFile } from "./storage.js";
|
|
28
|
+
|
|
29
|
+
// Commands & downloads
|
|
30
|
+
export { default as CommandHandler } from "./command-handler.js";
|
|
31
|
+
export type { CommandResult } from "./command-handler.js";
|
|
32
|
+
export { default as DownloadHandler } from "./download-handler.js";
|
|
33
|
+
|
|
34
|
+
// Protocol types
|
|
35
|
+
export type {
|
|
36
|
+
IncomingMessage,
|
|
37
|
+
OutgoingMessage,
|
|
38
|
+
InvocationMessage,
|
|
39
|
+
CancelMessage,
|
|
40
|
+
ResultMessage,
|
|
41
|
+
AssistantMessage,
|
|
42
|
+
ArtifactUploadRequestMessage,
|
|
43
|
+
ArtifactUploadResponseMessage,
|
|
44
|
+
ErrorMessage,
|
|
45
|
+
StreamEndMessage,
|
|
46
|
+
Command,
|
|
47
|
+
RuntimeEnvironmentDownloadable,
|
|
48
|
+
} from "./types.js";
|
|
49
|
+
|
|
50
|
+
// Utilities
|
|
51
|
+
export { redactSecrets, sleep } from "./utils.js";
|
|
52
|
+
export { createLogger, defaultLogger } from "./logger.js";
|
|
53
|
+
export type { Logger } from "./logger.js";
|
|
54
|
+
|
|
55
|
+
// Constants
|
|
56
|
+
export {
|
|
57
|
+
DEFAULT_ARTIFACTS_DIR,
|
|
58
|
+
DEFAULT_ARTIFACT_IGNORE,
|
|
59
|
+
} from "./constants.js";
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface Logger {
|
|
2
|
+
log(...args: unknown[]): void;
|
|
3
|
+
error(...args: unknown[]): void;
|
|
4
|
+
debug(...args: unknown[]): void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function createLogger(sourceId: string): Logger {
|
|
8
|
+
const prefix = `[${sourceId}]`;
|
|
9
|
+
return {
|
|
10
|
+
log: (...args: unknown[]) => console.log(prefix, ...args),
|
|
11
|
+
error: (...args: unknown[]) => console.error(prefix, ...args),
|
|
12
|
+
debug: (...args: unknown[]) => console.debug(prefix, ...args),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const defaultLogger: Logger = {
|
|
17
|
+
log: (...args: unknown[]) => console.log(...args),
|
|
18
|
+
error: (...args: unknown[]) => console.error(...args),
|
|
19
|
+
debug: (...args: unknown[]) => console.debug(...args),
|
|
20
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import zod from "zod";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Agent,
|
|
5
|
+
run as runAgent,
|
|
6
|
+
codeInterpreterTool,
|
|
7
|
+
webSearchTool,
|
|
8
|
+
AgentOutputType,
|
|
9
|
+
} from "@openai/agents";
|
|
10
|
+
import type {
|
|
11
|
+
AgentHandler,
|
|
12
|
+
AgentInvocation,
|
|
13
|
+
AgentResult,
|
|
14
|
+
MessageSender,
|
|
15
|
+
} from "./agent-handler.js";
|
|
16
|
+
|
|
17
|
+
export const openaiHandler: AgentHandler = {
|
|
18
|
+
async run(
|
|
19
|
+
invocation: AgentInvocation,
|
|
20
|
+
sender: MessageSender,
|
|
21
|
+
): Promise<AgentResult> {
|
|
22
|
+
Object.assign(process.env, invocation.env);
|
|
23
|
+
|
|
24
|
+
const strictSchema = ensureStrictSchema(invocation.outputFormat.schema);
|
|
25
|
+
const outputType = zod.fromJSONSchema(strictSchema) as AgentOutputType;
|
|
26
|
+
|
|
27
|
+
const agent = new Agent({
|
|
28
|
+
name: "runtimeuse-agent",
|
|
29
|
+
instructions: invocation.systemPrompt,
|
|
30
|
+
outputType,
|
|
31
|
+
model: invocation.model,
|
|
32
|
+
tools: [codeInterpreterTool(), webSearchTool()],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const result = await runAgent(agent, invocation.userPrompt, {
|
|
36
|
+
signal: invocation.signal,
|
|
37
|
+
stream: true,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
let currentText = "";
|
|
41
|
+
for await (const event of result) {
|
|
42
|
+
if (
|
|
43
|
+
event.type === "raw_model_stream_event" &&
|
|
44
|
+
"delta" in event.data &&
|
|
45
|
+
typeof event.data.delta === "string"
|
|
46
|
+
) {
|
|
47
|
+
currentText += event.data.delta;
|
|
48
|
+
} else if (
|
|
49
|
+
event.type === "run_item_stream_event" &&
|
|
50
|
+
event.name === "message_output_created"
|
|
51
|
+
) {
|
|
52
|
+
if (currentText) {
|
|
53
|
+
sender.sendAssistantMessage([currentText]);
|
|
54
|
+
currentText = "";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await result.completed;
|
|
60
|
+
|
|
61
|
+
if (currentText) {
|
|
62
|
+
sender.sendAssistantMessage([currentText]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let structuredOutput: Record<string, unknown> = {};
|
|
66
|
+
const finalOutput = result.finalOutput;
|
|
67
|
+
|
|
68
|
+
if (typeof finalOutput === "string") {
|
|
69
|
+
try {
|
|
70
|
+
structuredOutput = JSON.parse(finalOutput);
|
|
71
|
+
} catch {
|
|
72
|
+
structuredOutput = { result: finalOutput };
|
|
73
|
+
}
|
|
74
|
+
} else if (finalOutput != null && typeof finalOutput === "object") {
|
|
75
|
+
structuredOutput = finalOutput as Record<string, unknown>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const metadata: Record<string, unknown> = {};
|
|
79
|
+
const usage = result.state?.usage;
|
|
80
|
+
if (usage) {
|
|
81
|
+
metadata.usage = {
|
|
82
|
+
inputTokens: usage.inputTokens,
|
|
83
|
+
outputTokens: usage.outputTokens,
|
|
84
|
+
totalTokens: usage.totalTokens,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { structuredOutput, metadata };
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
function ensureStrictSchema(
|
|
93
|
+
schema: Record<string, unknown>,
|
|
94
|
+
): Record<string, unknown> {
|
|
95
|
+
const result = { ...schema };
|
|
96
|
+
if (result.type === "object") {
|
|
97
|
+
result.additionalProperties = false;
|
|
98
|
+
if (
|
|
99
|
+
result.properties &&
|
|
100
|
+
typeof result.properties === "object" &&
|
|
101
|
+
!Array.isArray(result.properties)
|
|
102
|
+
) {
|
|
103
|
+
const props: Record<string, unknown> = {};
|
|
104
|
+
for (const [key, value] of Object.entries(
|
|
105
|
+
result.properties as Record<string, unknown>,
|
|
106
|
+
)) {
|
|
107
|
+
props[key] =
|
|
108
|
+
value && typeof value === "object" && !Array.isArray(value)
|
|
109
|
+
? ensureStrictSchema(value as Record<string, unknown>)
|
|
110
|
+
: value;
|
|
111
|
+
}
|
|
112
|
+
result.properties = props;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (result.items && typeof result.items === "object") {
|
|
116
|
+
result.items = ensureStrictSchema(result.items as Record<string, unknown>);
|
|
117
|
+
}
|
|
118
|
+
delete result.title;
|
|
119
|
+
return result;
|
|
120
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
2
|
+
import type { AgentHandler } from "./agent-handler.js";
|
|
3
|
+
import { WebSocketSession, type SessionConfig } from "./session.js";
|
|
4
|
+
import { UploadTracker } from "./upload-tracker.js";
|
|
5
|
+
import { defaultLogger, type Logger } from "./logger.js";
|
|
6
|
+
|
|
7
|
+
export interface RuntimeUseServerConfig {
|
|
8
|
+
handler: AgentHandler;
|
|
9
|
+
port?: number;
|
|
10
|
+
defaultModel?: string;
|
|
11
|
+
uploadTimeoutMs?: number;
|
|
12
|
+
artifactWaitMs?: number;
|
|
13
|
+
postInvocationDelayMs?: number;
|
|
14
|
+
logger?: Logger;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class RuntimeUseServer {
|
|
18
|
+
private readonly wss: WebSocketServer;
|
|
19
|
+
private readonly config: RuntimeUseServerConfig;
|
|
20
|
+
private readonly logger: Logger;
|
|
21
|
+
|
|
22
|
+
constructor(config: RuntimeUseServerConfig) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.logger = config.logger ?? defaultLogger;
|
|
25
|
+
this.wss = new WebSocketServer({ port: config.port ?? 8080 });
|
|
26
|
+
|
|
27
|
+
this.wss.on("connection", (ws: WebSocket) => {
|
|
28
|
+
this.handleConnection(ws);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private handleConnection(ws: WebSocket): void {
|
|
33
|
+
const uploadTracker = new UploadTracker();
|
|
34
|
+
const sessionConfig: SessionConfig = {
|
|
35
|
+
handler: this.config.handler,
|
|
36
|
+
uploadTracker,
|
|
37
|
+
defaultModel: this.config.defaultModel,
|
|
38
|
+
uploadTimeoutMs: this.config.uploadTimeoutMs,
|
|
39
|
+
artifactWaitMs: this.config.artifactWaitMs,
|
|
40
|
+
postInvocationDelayMs: this.config.postInvocationDelayMs,
|
|
41
|
+
logger: this.config.logger,
|
|
42
|
+
};
|
|
43
|
+
const session = new WebSocketSession(ws, sessionConfig);
|
|
44
|
+
session.run().catch((error) => {
|
|
45
|
+
this.logger.error("Session error:", error);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async start(): Promise<void> {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
this.wss.on("listening", () => {
|
|
52
|
+
this.logger.log(
|
|
53
|
+
`RuntimeUse server listening on port ${this.config.port ?? 8080}`,
|
|
54
|
+
);
|
|
55
|
+
resolve();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async stop(): Promise<void> {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
this.wss.close((err) => {
|
|
63
|
+
if (err) reject(err);
|
|
64
|
+
else resolve();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|