@anureo/sdk 0.0.0-stage → 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/LICENSE +201 -0
- package/README.md +88 -2
- package/dist/diagnostics.d.ts +15 -0
- package/dist/diagnostics.js +34 -0
- package/dist/errors.d.ts +15 -0
- package/dist/file-change-limits.d.ts +5 -0
- package/dist/identity.d.ts +13 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1474 -0
- package/dist/interactions.d.ts +20 -0
- package/dist/ports.d.ts +66 -0
- package/dist/process.d.ts +34 -0
- package/dist/process.js +191 -0
- package/dist/queue.d.ts +36 -0
- package/dist/queue.js +166 -0
- package/dist/runtime.d.ts +85 -0
- package/dist/secrets.d.ts +28 -0
- package/package.json +61 -4
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type InteractionRequested, type InteractionResponse, type InteractionResult } from "@anureo/protocol";
|
|
2
|
+
import type { NativeInteractionResponder } from "./ports.js";
|
|
3
|
+
export declare class InteractionRegistry {
|
|
4
|
+
private readonly publish;
|
|
5
|
+
private readonly limits;
|
|
6
|
+
private readonly beforeResponse?;
|
|
7
|
+
private readonly records;
|
|
8
|
+
constructor(publish: (result: InteractionResult) => Promise<void>, limits: {
|
|
9
|
+
maxPending: number;
|
|
10
|
+
timeoutMs: number;
|
|
11
|
+
responseTimeoutMs?: number;
|
|
12
|
+
}, beforeResponse?: ((request: InteractionRequested, response: InteractionResponse) => void) | undefined);
|
|
13
|
+
get pendingCount(): number;
|
|
14
|
+
pending(): InteractionRequested[];
|
|
15
|
+
begin(request: InteractionRequested, responder: NativeInteractionResponder): Promise<InteractionResponse>;
|
|
16
|
+
respond(id: string, requestId: string, response: InteractionResponse): Promise<InteractionResult>;
|
|
17
|
+
cancel(id: string, reason?: string, outcome?: "cancelled" | "expired" | "unknown"): Promise<InteractionResult>;
|
|
18
|
+
cancelAll(reason: string, turnId?: string): Promise<void>;
|
|
19
|
+
resolveExternally(result: InteractionResult): Promise<void>;
|
|
20
|
+
}
|
package/dist/ports.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Capabilities, AhpEvent, AhpEventPayload, AhpMethod, AhpRequestMap, AhpResultMap, InitializeParams, InitializeResult, InteractionRequested, InteractionResponse, SessionBinding, SessionOpenParams } from "@anureo/protocol";
|
|
2
|
+
import type { RequestOptions } from "./errors.js";
|
|
3
|
+
import type { QueueLimits } from "./queue.js";
|
|
4
|
+
export type SessionMethod = Exclude<AhpMethod, "initialize" | "session/open">;
|
|
5
|
+
export interface EventScope {
|
|
6
|
+
turnId?: string;
|
|
7
|
+
itemId?: string;
|
|
8
|
+
}
|
|
9
|
+
export type NativeInteractionResponder = (response: InteractionResponse | null, reason?: string) => Promise<void>;
|
|
10
|
+
export interface SessionEventSink {
|
|
11
|
+
emit(event: AhpEventPayload, scope?: EventScope): Promise<void>;
|
|
12
|
+
/** Non-pausable callback: false means the source must stop and the stream has failed. */
|
|
13
|
+
tryEmit(event: AhpEventPayload, scope?: EventScope): boolean;
|
|
14
|
+
requestInteraction(request: InteractionRequested, responder: NativeInteractionResponder): Promise<InteractionResponse>;
|
|
15
|
+
/** A remote peer resolved/expired an existing request. Never sends a second native response. */
|
|
16
|
+
resolveInteraction(result: AhpResultMap["interaction/respond"]): Promise<void>;
|
|
17
|
+
fail(cause: unknown): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export interface BackendSession {
|
|
20
|
+
readonly binding: SessionBinding;
|
|
21
|
+
readonly capabilities: Capabilities;
|
|
22
|
+
execute<M extends SessionMethod>(method: M, params: AhpRequestMap[M]): Promise<AhpResultMap[M]>;
|
|
23
|
+
dispose(): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
export interface HarnessAdapter {
|
|
26
|
+
initialize(params: InitializeParams): Promise<InitializeResult>;
|
|
27
|
+
openSession(params: SessionOpenParams, sink: SessionEventSink): Promise<BackendSession>;
|
|
28
|
+
dispose(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
export interface RuntimeOptions {
|
|
31
|
+
createId?: () => string;
|
|
32
|
+
now?: () => string;
|
|
33
|
+
requestTimeoutMs?: number;
|
|
34
|
+
openTimeoutMs?: number;
|
|
35
|
+
interactionTimeoutMs?: number;
|
|
36
|
+
maxPendingInteractions?: number;
|
|
37
|
+
requestCacheSize?: number;
|
|
38
|
+
queue?: QueueLimits;
|
|
39
|
+
/** Runs synchronously before native submission; persist only irreversible privacy metadata. */
|
|
40
|
+
onMarkedSecret?: (binding: Readonly<SessionBinding>, value: string) => void;
|
|
41
|
+
/** Host-owned cross-process privacy projection for authoritative native history. */
|
|
42
|
+
redactHistory?: (binding: Readonly<SessionBinding>, page: AhpResultMap["history/read"]) => AhpResultMap["history/read"] | Promise<AhpResultMap["history/read"]>;
|
|
43
|
+
}
|
|
44
|
+
export interface HarnessSession {
|
|
45
|
+
readonly binding: Readonly<SessionBinding>;
|
|
46
|
+
readonly capabilities: Readonly<Capabilities>;
|
|
47
|
+
events(): AsyncIterable<AhpEvent>;
|
|
48
|
+
startTurn(params: AhpRequestMap["turn/start"], options?: RequestOptions): Promise<AhpResultMap["turn/start"]>;
|
|
49
|
+
steer(params: AhpRequestMap["turn/steer"], options?: RequestOptions): Promise<AhpResultMap["turn/steer"]>;
|
|
50
|
+
interrupt(params: AhpRequestMap["turn/interrupt"], options?: RequestOptions): Promise<AhpResultMap["turn/interrupt"]>;
|
|
51
|
+
respond(params: AhpRequestMap["interaction/respond"], options?: RequestOptions): Promise<AhpResultMap["interaction/respond"]>;
|
|
52
|
+
cancelInteraction(params: AhpRequestMap["interaction/cancel"], options?: RequestOptions): Promise<AhpResultMap["interaction/cancel"]>;
|
|
53
|
+
setConfig(params: AhpRequestMap["session/config/set"], options?: RequestOptions): Promise<AhpResultMap["session/config/set"]>;
|
|
54
|
+
revert(params: AhpRequestMap["session/revert"], options?: RequestOptions): Promise<AhpResultMap["session/revert"]>;
|
|
55
|
+
readHistory(params: AhpRequestMap["history/read"], options?: RequestOptions): Promise<AhpResultMap["history/read"]>;
|
|
56
|
+
setGoal(params: AhpRequestMap["goal/set"], options?: RequestOptions): Promise<AhpResultMap["goal/set"]>;
|
|
57
|
+
clearGoal(params: AhpRequestMap["goal/clear"], options?: RequestOptions): Promise<AhpResultMap["goal/clear"]>;
|
|
58
|
+
compact(params: AhpRequestMap["compact/start"], options?: RequestOptions): Promise<AhpResultMap["compact/start"]>;
|
|
59
|
+
review(params: AhpRequestMap["review/start"], options?: RequestOptions): Promise<AhpResultMap["review/start"]>;
|
|
60
|
+
close(options?: RequestOptions): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
export interface HarnessRuntime {
|
|
63
|
+
initialize(params: InitializeParams, options?: RequestOptions): Promise<InitializeResult>;
|
|
64
|
+
openSession(params: SessionOpenParams, options?: RequestOptions): Promise<HarnessSession>;
|
|
65
|
+
dispose(): Promise<void>;
|
|
66
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type ProcessExit = {
|
|
2
|
+
code: number | null;
|
|
3
|
+
signal: string | null;
|
|
4
|
+
errorCode?: string;
|
|
5
|
+
};
|
|
6
|
+
export interface HarnessProcessOptions {
|
|
7
|
+
command: string;
|
|
8
|
+
args?: readonly string[];
|
|
9
|
+
cwd?: string;
|
|
10
|
+
/** A complete child environment; omitted means inherit. Never mutates the parent environment. */
|
|
11
|
+
env?: Record<string, string | undefined>;
|
|
12
|
+
/** False for a process whose lifetime belongs to an external owner. */
|
|
13
|
+
owned?: boolean;
|
|
14
|
+
shutdownTimeoutMs?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface HarnessProcess {
|
|
17
|
+
readonly pid: number;
|
|
18
|
+
readonly owned: boolean;
|
|
19
|
+
readonly stdout: AsyncIterable<Uint8Array>;
|
|
20
|
+
readonly stderr: AsyncIterable<Uint8Array>;
|
|
21
|
+
/** Resolves at process exit; consumers may still drain buffered stdout/stderr. */
|
|
22
|
+
readonly exited: Promise<ProcessExit>;
|
|
23
|
+
write(chunk: string | Uint8Array): Promise<void>;
|
|
24
|
+
closeInput(): Promise<void>;
|
|
25
|
+
/** Idempotent: terminate an owned process tree, detach an externally owned process. */
|
|
26
|
+
dispose(): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
export declare class ProcessPortError extends Error {
|
|
29
|
+
readonly code: 'HARNESS_UNAVAILABLE' | 'PROCESS_WRITE_FAILED' | 'PROCESS_DISPOSE_FAILED';
|
|
30
|
+
readonly nativeCode?: string;
|
|
31
|
+
constructor(code: ProcessPortError['code'], message: string, options?: ErrorOptions);
|
|
32
|
+
}
|
|
33
|
+
/** Node's child_process implementation is also supported by Bun; no shell parsing is involved. */
|
|
34
|
+
export declare function spawnHarnessProcess(options: HarnessProcessOptions): Promise<HarnessProcess>;
|
package/dist/process.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// packages/core/src/process.ts
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
4
|
+
|
|
5
|
+
class ProcessPortError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
nativeCode;
|
|
8
|
+
constructor(code, message, options) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "ProcessPortError";
|
|
11
|
+
this.code = code;
|
|
12
|
+
const cause = options?.cause;
|
|
13
|
+
if (cause && typeof cause === "object" && "code" in cause && typeof cause.code === "string" && /^[A-Z][A-Z0-9_]{0,63}$/.test(cause.code))
|
|
14
|
+
this.nativeCode = cause.code;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function exists(pid) {
|
|
18
|
+
try {
|
|
19
|
+
process.kill(pid, 0);
|
|
20
|
+
return true;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return error.code === "EPERM";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function signalGroup(pid, signal) {
|
|
26
|
+
try {
|
|
27
|
+
process.kill(-pid, signal);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error.code !== "ESRCH")
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function signalGroupWithExitRace(pid, signal) {
|
|
34
|
+
for (let attempt = 0;; attempt++) {
|
|
35
|
+
try {
|
|
36
|
+
signalGroup(pid, signal);
|
|
37
|
+
return;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error.code !== "EPERM" || attempt >= 4)
|
|
40
|
+
throw error;
|
|
41
|
+
await delay(10);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function windowsKillTree(pid, force) {
|
|
46
|
+
await new Promise((resolve, reject) => {
|
|
47
|
+
const killer = spawn("taskkill.exe", ["/PID", String(pid), "/T", ...force ? ["/F"] : []], {
|
|
48
|
+
stdio: "ignore",
|
|
49
|
+
windowsHide: true,
|
|
50
|
+
shell: false
|
|
51
|
+
});
|
|
52
|
+
killer.once("error", reject);
|
|
53
|
+
killer.once("exit", (code) => {
|
|
54
|
+
if (code === 0 || !exists(pid))
|
|
55
|
+
resolve();
|
|
56
|
+
else
|
|
57
|
+
reject(new ProcessPortError("PROCESS_DISPOSE_FAILED", "Unable to terminate harness process tree"));
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async function terminateTree(child, pid, timeoutMs, didExit) {
|
|
62
|
+
if (!Number.isSafeInteger(pid) || pid <= 1)
|
|
63
|
+
throw new ProcessPortError("PROCESS_DISPOSE_FAILED", "Invalid original harness PID");
|
|
64
|
+
if (process.platform === "win32") {
|
|
65
|
+
if (!didExit()) {
|
|
66
|
+
try {
|
|
67
|
+
await windowsKillTree(pid, false);
|
|
68
|
+
} catch {
|
|
69
|
+
if (!didExit())
|
|
70
|
+
await windowsKillTree(pid, true);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const deadline = Date.now() + timeoutMs;
|
|
74
|
+
while (!didExit() && Date.now() < deadline)
|
|
75
|
+
await delay(10);
|
|
76
|
+
if (!didExit())
|
|
77
|
+
await windowsKillTree(pid, true);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
await signalGroupWithExitRace(pid, "SIGTERM");
|
|
81
|
+
const deadline = Date.now() + timeoutMs;
|
|
82
|
+
while (exists(-pid) && Date.now() < deadline)
|
|
83
|
+
await delay(10);
|
|
84
|
+
if (exists(-pid))
|
|
85
|
+
await signalGroupWithExitRace(pid, "SIGKILL");
|
|
86
|
+
}
|
|
87
|
+
async function spawnHarnessProcess(options) {
|
|
88
|
+
if (!options.command || options.command.includes("\x00") || options.args?.some((arg) => arg.includes("\x00"))) {
|
|
89
|
+
throw new ProcessPortError("HARNESS_UNAVAILABLE", "Invalid harness executable or argument");
|
|
90
|
+
}
|
|
91
|
+
const timeoutMs = options.shutdownTimeoutMs ?? 1000;
|
|
92
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 60000) {
|
|
93
|
+
throw new ProcessPortError("HARNESS_UNAVAILABLE", "Invalid process shutdown timeout");
|
|
94
|
+
}
|
|
95
|
+
const owned = options.owned ?? true;
|
|
96
|
+
let child;
|
|
97
|
+
try {
|
|
98
|
+
child = spawn(options.command, [...options.args ?? []], {
|
|
99
|
+
cwd: options.cwd,
|
|
100
|
+
env: options.env,
|
|
101
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
102
|
+
detached: owned && process.platform !== "win32",
|
|
103
|
+
shell: false,
|
|
104
|
+
windowsHide: true
|
|
105
|
+
});
|
|
106
|
+
} catch (cause) {
|
|
107
|
+
throw new ProcessPortError("HARNESS_UNAVAILABLE", "Unable to start harness executable", { cause });
|
|
108
|
+
}
|
|
109
|
+
let didExit = false;
|
|
110
|
+
let originalPid = child.pid;
|
|
111
|
+
let disposePromise;
|
|
112
|
+
const exited = new Promise((resolve) => {
|
|
113
|
+
child.once("exit", (code, signal) => {
|
|
114
|
+
didExit = true;
|
|
115
|
+
resolve({ code, signal });
|
|
116
|
+
});
|
|
117
|
+
child.once("error", (error) => {
|
|
118
|
+
if (!child.pid) {
|
|
119
|
+
didExit = true;
|
|
120
|
+
resolve({ code: null, signal: null, errorCode: error.code });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
child.stdin.on("error", () => {});
|
|
125
|
+
try {
|
|
126
|
+
await new Promise((resolve, reject) => {
|
|
127
|
+
child.once("spawn", () => {
|
|
128
|
+
if (child.pid !== undefined && child.pid > 1)
|
|
129
|
+
originalPid = child.pid;
|
|
130
|
+
resolve();
|
|
131
|
+
});
|
|
132
|
+
child.once("error", reject);
|
|
133
|
+
});
|
|
134
|
+
} catch (cause) {
|
|
135
|
+
child.stdin.destroy();
|
|
136
|
+
child.stdout.destroy();
|
|
137
|
+
child.stderr.destroy();
|
|
138
|
+
throw new ProcessPortError("HARNESS_UNAVAILABLE", "Unable to start harness executable", { cause });
|
|
139
|
+
}
|
|
140
|
+
const pid = originalPid;
|
|
141
|
+
const closeInput = () => new Promise((resolve, reject) => {
|
|
142
|
+
if (child.stdin.destroyed || child.stdin.writableEnded) {
|
|
143
|
+
resolve();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
child.stdin.end((error) => error ? reject(new ProcessPortError("PROCESS_WRITE_FAILED", "Unable to close harness input", { cause: error })) : resolve());
|
|
147
|
+
});
|
|
148
|
+
return {
|
|
149
|
+
pid,
|
|
150
|
+
owned,
|
|
151
|
+
stdout: child.stdout,
|
|
152
|
+
stderr: child.stderr,
|
|
153
|
+
exited,
|
|
154
|
+
write(chunk) {
|
|
155
|
+
if (disposePromise || child.stdin.destroyed || child.stdin.writableEnded || didExit) {
|
|
156
|
+
return Promise.reject(new ProcessPortError("PROCESS_WRITE_FAILED", "Harness process input is closed"));
|
|
157
|
+
}
|
|
158
|
+
return new Promise((resolve, reject) => {
|
|
159
|
+
child.stdin.write(chunk, (error) => error ? reject(new ProcessPortError("PROCESS_WRITE_FAILED", "Unable to write harness input", { cause: error })) : resolve());
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
closeInput,
|
|
163
|
+
dispose() {
|
|
164
|
+
return disposePromise ??= (async () => {
|
|
165
|
+
try {
|
|
166
|
+
if (owned) {
|
|
167
|
+
await terminateTree(child, pid, timeoutMs, () => didExit);
|
|
168
|
+
if (!didExit) {
|
|
169
|
+
await Promise.race([exited, delay(1000)]);
|
|
170
|
+
if (!didExit)
|
|
171
|
+
throw new ProcessPortError("PROCESS_DISPOSE_FAILED", "Harness process did not exit after termination");
|
|
172
|
+
}
|
|
173
|
+
} else
|
|
174
|
+
child.unref();
|
|
175
|
+
} catch (cause) {
|
|
176
|
+
if (cause instanceof ProcessPortError)
|
|
177
|
+
throw cause;
|
|
178
|
+
throw new ProcessPortError("PROCESS_DISPOSE_FAILED", "Unable to dispose harness process", { cause });
|
|
179
|
+
} finally {
|
|
180
|
+
child.stdin.destroy();
|
|
181
|
+
child.stdout.destroy();
|
|
182
|
+
child.stderr.destroy();
|
|
183
|
+
}
|
|
184
|
+
})();
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
export {
|
|
189
|
+
ProcessPortError,
|
|
190
|
+
spawnHarnessProcess
|
|
191
|
+
};
|
package/dist/queue.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface QueueLimits {
|
|
2
|
+
maxEvents?: number;
|
|
3
|
+
maxBytes?: number;
|
|
4
|
+
maxFrameBytes?: number;
|
|
5
|
+
maxWaitingWriters?: number;
|
|
6
|
+
maxControlEvents?: number;
|
|
7
|
+
}
|
|
8
|
+
/** A single-active-reader queue. Returning an iterator detaches; it does not cancel its producer. */
|
|
9
|
+
export declare class BoundedEventQueue<T> implements AsyncIterable<T> {
|
|
10
|
+
private readonly sizeOf;
|
|
11
|
+
private readonly entries;
|
|
12
|
+
private bytes;
|
|
13
|
+
private reader?;
|
|
14
|
+
private waiting?;
|
|
15
|
+
private controls;
|
|
16
|
+
private ended;
|
|
17
|
+
private failure?;
|
|
18
|
+
private writers;
|
|
19
|
+
readonly limits: Required<QueueLimits>;
|
|
20
|
+
constructor(limits?: QueueLimits, sizeOf?: (value: T) => number);
|
|
21
|
+
get bufferedEvents(): number;
|
|
22
|
+
get bufferedBytes(): number;
|
|
23
|
+
private wakeWriters;
|
|
24
|
+
private assertOpen;
|
|
25
|
+
private checkedSize;
|
|
26
|
+
/** For producers that cannot pause. Overflow is an explicit error, never an eviction. */
|
|
27
|
+
assertWritable(value: T): void;
|
|
28
|
+
push(value: T): void;
|
|
29
|
+
/** For producers which await the sink. The number of suspended writers is bounded too. */
|
|
30
|
+
write(value: T, accepted?: () => void): Promise<void>;
|
|
31
|
+
/** Reserved for a final failure event, even if the regular queue is full. */
|
|
32
|
+
pushControl(value: T): void;
|
|
33
|
+
endWithControl(value: T): void;
|
|
34
|
+
end(cause?: unknown): void;
|
|
35
|
+
[Symbol.asyncIterator](): AsyncIterator<T>;
|
|
36
|
+
}
|
package/dist/queue.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// packages/core/src/errors.ts
|
|
2
|
+
class HarnessSdkError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
retryable;
|
|
5
|
+
outcomeUnknown;
|
|
6
|
+
details;
|
|
7
|
+
name = "HarnessSdkError";
|
|
8
|
+
constructor(code, message = code, retryable = false, outcomeUnknown = false, details) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.retryable = retryable;
|
|
12
|
+
this.outcomeUnknown = outcomeUnknown;
|
|
13
|
+
this.details = details;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// packages/core/src/queue.ts
|
|
18
|
+
class BoundedEventQueue {
|
|
19
|
+
sizeOf;
|
|
20
|
+
entries = [];
|
|
21
|
+
bytes = 0;
|
|
22
|
+
reader;
|
|
23
|
+
waiting;
|
|
24
|
+
controls = [];
|
|
25
|
+
ended = false;
|
|
26
|
+
failure;
|
|
27
|
+
writers = new Set;
|
|
28
|
+
limits;
|
|
29
|
+
constructor(limits = {}, sizeOf = (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength) {
|
|
30
|
+
this.sizeOf = sizeOf;
|
|
31
|
+
this.limits = { maxEvents: limits.maxEvents ?? 2048, maxBytes: limits.maxBytes ?? 16 * 1024 * 1024, maxFrameBytes: limits.maxFrameBytes ?? 8 * 1024 * 1024, maxWaitingWriters: limits.maxWaitingWriters ?? 32, maxControlEvents: limits.maxControlEvents ?? 33 };
|
|
32
|
+
if (Object.values(this.limits).some((n) => !Number.isSafeInteger(n) || n <= 0))
|
|
33
|
+
throw new HarnessSdkError("INVALID_PARAMS", "Queue limits must be positive integers");
|
|
34
|
+
}
|
|
35
|
+
get bufferedEvents() {
|
|
36
|
+
return this.entries.length + this.controls.length;
|
|
37
|
+
}
|
|
38
|
+
get bufferedBytes() {
|
|
39
|
+
return this.bytes;
|
|
40
|
+
}
|
|
41
|
+
wakeWriters() {
|
|
42
|
+
for (const wake of this.writers)
|
|
43
|
+
wake();
|
|
44
|
+
this.writers.clear();
|
|
45
|
+
}
|
|
46
|
+
assertOpen() {
|
|
47
|
+
if (this.ended)
|
|
48
|
+
throw this.failure ?? new HarnessSdkError("STREAM_CLOSED");
|
|
49
|
+
}
|
|
50
|
+
checkedSize(value) {
|
|
51
|
+
const bytes = this.sizeOf(value);
|
|
52
|
+
if (bytes > this.limits.maxFrameBytes || bytes > this.limits.maxBytes)
|
|
53
|
+
throw new HarnessSdkError("FRAME_TOO_LARGE");
|
|
54
|
+
return bytes;
|
|
55
|
+
}
|
|
56
|
+
assertWritable(value) {
|
|
57
|
+
this.assertOpen();
|
|
58
|
+
const bytes = this.checkedSize(value);
|
|
59
|
+
if (!this.waiting && (this.entries.length >= this.limits.maxEvents || this.bytes + bytes > this.limits.maxBytes))
|
|
60
|
+
throw new HarnessSdkError("BACKPRESSURE_EXCEEDED");
|
|
61
|
+
}
|
|
62
|
+
push(value) {
|
|
63
|
+
this.assertWritable(value);
|
|
64
|
+
const bytes = this.checkedSize(value);
|
|
65
|
+
if (this.waiting) {
|
|
66
|
+
const waiting = this.waiting;
|
|
67
|
+
this.waiting = undefined;
|
|
68
|
+
waiting.resolve({ done: false, value });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.entries.push({ value, bytes });
|
|
72
|
+
this.bytes += bytes;
|
|
73
|
+
}
|
|
74
|
+
async write(value, accepted) {
|
|
75
|
+
const bytes = this.checkedSize(value);
|
|
76
|
+
while (!this.waiting && (this.entries.length >= this.limits.maxEvents || this.bytes + bytes > this.limits.maxBytes)) {
|
|
77
|
+
this.assertOpen();
|
|
78
|
+
if (this.writers.size >= this.limits.maxWaitingWriters)
|
|
79
|
+
throw new HarnessSdkError("BACKPRESSURE_EXCEEDED");
|
|
80
|
+
await new Promise((resolve) => this.writers.add(resolve));
|
|
81
|
+
}
|
|
82
|
+
this.push(value);
|
|
83
|
+
accepted?.();
|
|
84
|
+
}
|
|
85
|
+
pushControl(value) {
|
|
86
|
+
this.assertOpen();
|
|
87
|
+
this.checkedSize(value);
|
|
88
|
+
if (this.waiting) {
|
|
89
|
+
const waiter = this.waiting;
|
|
90
|
+
this.waiting = undefined;
|
|
91
|
+
waiter.resolve({ done: false, value });
|
|
92
|
+
} else {
|
|
93
|
+
if (this.controls.length >= this.limits.maxControlEvents)
|
|
94
|
+
throw new HarnessSdkError("BACKPRESSURE_EXCEEDED", "Control event limit exceeded");
|
|
95
|
+
this.controls.push(value);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
endWithControl(value) {
|
|
99
|
+
if (this.ended)
|
|
100
|
+
return;
|
|
101
|
+
this.pushControl(value);
|
|
102
|
+
this.end();
|
|
103
|
+
}
|
|
104
|
+
end(cause) {
|
|
105
|
+
if (this.ended)
|
|
106
|
+
return;
|
|
107
|
+
this.ended = true;
|
|
108
|
+
this.failure = cause;
|
|
109
|
+
this.wakeWriters();
|
|
110
|
+
if (this.waiting) {
|
|
111
|
+
const waiter = this.waiting;
|
|
112
|
+
this.waiting = undefined;
|
|
113
|
+
cause ? waiter.reject(cause) : waiter.resolve({ done: true, value: undefined });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
[Symbol.asyncIterator]() {
|
|
117
|
+
if (this.reader)
|
|
118
|
+
throw new HarnessSdkError("EVENT_STREAM_ALREADY_OWNED");
|
|
119
|
+
const reader = Symbol("reader");
|
|
120
|
+
this.reader = reader;
|
|
121
|
+
let detached = false;
|
|
122
|
+
const detach = () => {
|
|
123
|
+
detached = true;
|
|
124
|
+
if (this.reader === reader)
|
|
125
|
+
this.reader = undefined;
|
|
126
|
+
if (this.waiting?.reader === reader) {
|
|
127
|
+
const waiter = this.waiting;
|
|
128
|
+
this.waiting = undefined;
|
|
129
|
+
waiter.resolve({ done: true, value: undefined });
|
|
130
|
+
}
|
|
131
|
+
return { done: true, value: undefined };
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
next: () => {
|
|
135
|
+
if (detached)
|
|
136
|
+
return Promise.resolve({ done: true, value: undefined });
|
|
137
|
+
const entry = this.entries.shift();
|
|
138
|
+
if (entry) {
|
|
139
|
+
this.bytes -= entry.bytes;
|
|
140
|
+
this.wakeWriters();
|
|
141
|
+
return Promise.resolve({ done: false, value: entry.value });
|
|
142
|
+
}
|
|
143
|
+
if (this.controls.length)
|
|
144
|
+
return Promise.resolve({ done: false, value: this.controls.shift() });
|
|
145
|
+
if (this.ended) {
|
|
146
|
+
detach();
|
|
147
|
+
return this.failure ? Promise.reject(this.failure) : Promise.resolve({ done: true, value: undefined });
|
|
148
|
+
}
|
|
149
|
+
if (this.waiting)
|
|
150
|
+
return Promise.reject(new HarnessSdkError("CONCURRENT_EVENT_READ"));
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
this.waiting = { reader, resolve, reject };
|
|
153
|
+
this.wakeWriters();
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
return: async () => detach(),
|
|
157
|
+
throw: async (cause) => {
|
|
158
|
+
detach();
|
|
159
|
+
throw cause;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
export {
|
|
165
|
+
BoundedEventQueue
|
|
166
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type Capabilities, type AhpEvent, type AhpEventPayload, type AhpRequestMap, type AhpResultMap, type InitializeParams, type InitializeResult, type InteractionRequested, type InteractionResponse, type SessionBinding, type SessionOpenParams, type SessionState } from "@anureo/protocol";
|
|
2
|
+
import { type RequestOptions } from "./errors.js";
|
|
3
|
+
import type { BackendSession, EventScope, HarnessAdapter, HarnessRuntime, HarnessSession, NativeInteractionResponder, RuntimeOptions, SessionEventSink } from "./ports.js";
|
|
4
|
+
export declare class ManagedHarnessSession implements HarnessSession, SessionEventSink {
|
|
5
|
+
private readonly options;
|
|
6
|
+
private readonly clientCapabilities?;
|
|
7
|
+
private _binding;
|
|
8
|
+
private _capabilities;
|
|
9
|
+
private readonly queue;
|
|
10
|
+
private readonly interactions;
|
|
11
|
+
private readonly streamId;
|
|
12
|
+
private seq;
|
|
13
|
+
private activeTurnId?;
|
|
14
|
+
private readonly started;
|
|
15
|
+
private readonly ended;
|
|
16
|
+
private readonly deliveredStarted;
|
|
17
|
+
private readonly deliveredEnded;
|
|
18
|
+
private readonly items;
|
|
19
|
+
private backend?;
|
|
20
|
+
private closed;
|
|
21
|
+
private recovering;
|
|
22
|
+
private controlBusy;
|
|
23
|
+
private maintenancePending;
|
|
24
|
+
private emitter;
|
|
25
|
+
private pendingEmissions;
|
|
26
|
+
private disposePromise?;
|
|
27
|
+
private failurePromise?;
|
|
28
|
+
private failureCause?;
|
|
29
|
+
private emergencyPublication;
|
|
30
|
+
private backendDisposal?;
|
|
31
|
+
private requests;
|
|
32
|
+
private state;
|
|
33
|
+
private readonly createId;
|
|
34
|
+
private readonly secrets;
|
|
35
|
+
constructor(binding: SessionBinding, capabilities: Capabilities, options?: RuntimeOptions, initial?: Pick<SessionOpenParams, "config" | "model">, clientCapabilities?: Capabilities | undefined);
|
|
36
|
+
get binding(): Readonly<SessionBinding>;
|
|
37
|
+
get capabilities(): Readonly<Capabilities>;
|
|
38
|
+
get isClosed(): boolean;
|
|
39
|
+
get snapshot(): SessionState;
|
|
40
|
+
events(): AsyncIterable<AhpEvent>;
|
|
41
|
+
reserveFork(binding: SessionBinding): () => void;
|
|
42
|
+
attach(backend: BackendSession): Promise<void>;
|
|
43
|
+
private makeEvent;
|
|
44
|
+
private prepare;
|
|
45
|
+
private accepted;
|
|
46
|
+
emit(payload: AhpEventPayload, scope?: EventScope): Promise<void>;
|
|
47
|
+
tryEmit(payload: AhpEventPayload, scope?: EventScope): boolean;
|
|
48
|
+
requestInteraction(request: InteractionRequested, responder: NativeInteractionResponder): Promise<InteractionResponse>;
|
|
49
|
+
resolveInteraction(result: AhpResultMap["interaction/respond"]): Promise<void>;
|
|
50
|
+
fail(cause: unknown): Promise<void>;
|
|
51
|
+
private performFailure;
|
|
52
|
+
private requireBackend;
|
|
53
|
+
private guard;
|
|
54
|
+
private call;
|
|
55
|
+
startTurn(params: AhpRequestMap["turn/start"], options?: RequestOptions): Promise<import("@anureo/protocol").TurnStartResult>;
|
|
56
|
+
steer(params: AhpRequestMap["turn/steer"], options?: RequestOptions): Promise<import("@anureo/protocol").TurnSteerResult>;
|
|
57
|
+
interrupt(params: AhpRequestMap["turn/interrupt"], options?: RequestOptions): Promise<import("@anureo/protocol").TurnInterruptResult>;
|
|
58
|
+
respond(params: AhpRequestMap["interaction/respond"], options?: RequestOptions): Promise<import("@anureo/protocol").InteractionResult>;
|
|
59
|
+
cancelInteraction(params: AhpRequestMap["interaction/cancel"], options?: RequestOptions): Promise<import("@anureo/protocol").InteractionResult>;
|
|
60
|
+
setConfig(params: AhpRequestMap["session/config/set"], options?: RequestOptions): Promise<SessionState>;
|
|
61
|
+
revert(params: AhpRequestMap["session/revert"], options?: RequestOptions): Promise<import("@anureo/protocol").RevertResult>;
|
|
62
|
+
readHistory(params: AhpRequestMap["history/read"], options?: RequestOptions): Promise<import("@anureo/protocol").HistoryPage>;
|
|
63
|
+
setGoal(params: AhpRequestMap["goal/set"], options?: RequestOptions): Promise<import("@anureo/protocol").GoalState>;
|
|
64
|
+
clearGoal(params: AhpRequestMap["goal/clear"], options?: RequestOptions): Promise<import("@anureo/protocol").GoalState>;
|
|
65
|
+
compact(params: AhpRequestMap["compact/start"], options?: RequestOptions): Promise<import("@anureo/protocol").MaintenanceResult>;
|
|
66
|
+
review(params: AhpRequestMap["review/start"], options?: RequestOptions): Promise<import("@anureo/protocol").TurnStartResult>;
|
|
67
|
+
close(options?: RequestOptions): Promise<void>;
|
|
68
|
+
private disposeBackend;
|
|
69
|
+
dispose(): Promise<void>;
|
|
70
|
+
}
|
|
71
|
+
export declare class ManagedHarnessRuntime implements HarnessRuntime {
|
|
72
|
+
private readonly adapter;
|
|
73
|
+
private readonly options;
|
|
74
|
+
private initialized?;
|
|
75
|
+
private readonly sessions;
|
|
76
|
+
private closed;
|
|
77
|
+
private disposing?;
|
|
78
|
+
private initialization?;
|
|
79
|
+
private clientCapabilities?;
|
|
80
|
+
constructor(adapter: HarnessAdapter, options?: RuntimeOptions);
|
|
81
|
+
initialize(params: InitializeParams, options?: RequestOptions): Promise<InitializeResult>;
|
|
82
|
+
openSession(params: SessionOpenParams, options?: RequestOptions): Promise<HarnessSession>;
|
|
83
|
+
dispose(): Promise<void>;
|
|
84
|
+
}
|
|
85
|
+
export declare function createHarnessRuntime(adapter: HarnessAdapter, options?: RuntimeOptions): HarnessRuntime;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Capabilities, AhpEventPayload, AhpMethod, AhpResultMap, GoalState, HistoryPage, InteractionRequested, InteractionResponse, Item, JsonValue, SessionState } from "@anureo/protocol";
|
|
2
|
+
/** Session-local marked answers only. Values never persist and unmarked chat text is not inferred. */
|
|
3
|
+
export declare class SecretRedactor {
|
|
4
|
+
private readonly onRemember?;
|
|
5
|
+
private readonly values;
|
|
6
|
+
private readonly streams;
|
|
7
|
+
private secretBytes;
|
|
8
|
+
constructor(onRemember?: ((secret: string) => void) | undefined);
|
|
9
|
+
private remember;
|
|
10
|
+
rememberResponse(request: InteractionRequested, response: InteractionResponse): void;
|
|
11
|
+
private fromSchema;
|
|
12
|
+
private scan;
|
|
13
|
+
text(value: string): string;
|
|
14
|
+
delta(stream: string, value: string): string;
|
|
15
|
+
json(value: JsonValue): JsonValue;
|
|
16
|
+
private schema;
|
|
17
|
+
capabilities(value: Capabilities): Capabilities;
|
|
18
|
+
goal(value: GoalState): GoalState;
|
|
19
|
+
state(value: SessionState): SessionState;
|
|
20
|
+
/** Redact content fields while preserving IDs, protocol enums, revisions and schema constraints. */
|
|
21
|
+
result<M extends AhpMethod>(method: M, value: AhpResultMap[M]): AhpResultMap[M];
|
|
22
|
+
private input;
|
|
23
|
+
item(item: Item): Item;
|
|
24
|
+
interaction(request: InteractionRequested): InteractionRequested;
|
|
25
|
+
private deltaValue;
|
|
26
|
+
event(event: AhpEventPayload): AhpEventPayload;
|
|
27
|
+
history(page: HistoryPage): HistoryPage;
|
|
28
|
+
}
|