@hunterzhu/pulse-adapters 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.
@@ -0,0 +1,225 @@
1
+ import { assignRuntimeToolCallIds, ModelFallbackController, OutputValidationError, estimateProjectionTokens, stableSerialize, validateAdapterResult, validateJsonSchema, modelFallbackError, runtimeErrorFromCause } from '@hunterzhu/pulse-runtime';
2
+ class AsyncSlot {
3
+ limit;
4
+ active = 0;
5
+ pending = [];
6
+ constructor(limit) {
7
+ this.limit = limit;
8
+ }
9
+ async acquire(signal) {
10
+ if (this.limit === Number.POSITIVE_INFINITY || this.active < this.limit) {
11
+ this.active++;
12
+ return () => this.release();
13
+ }
14
+ if (signal?.aborted)
15
+ throw new Error('EFFECT_CANCELLED');
16
+ return new Promise((resolve, reject) => {
17
+ const request = { signal, resolve, reject };
18
+ const onAbort = () => { const index = this.pending.indexOf(request); if (index >= 0)
19
+ this.pending.splice(index, 1); reject(new Error('EFFECT_CANCELLED')); };
20
+ if (signal)
21
+ signal.addEventListener('abort', onAbort, { once: true });
22
+ this.pending.push(request);
23
+ });
24
+ }
25
+ release() {
26
+ const next = this.pending.shift();
27
+ if (!next) {
28
+ this.active = Math.max(0, this.active - 1);
29
+ return;
30
+ }
31
+ if (next.signal?.aborted) {
32
+ next.reject(new Error('EFFECT_CANCELLED'));
33
+ this.release();
34
+ return;
35
+ }
36
+ next.resolve(() => this.release());
37
+ }
38
+ }
39
+ class SlotPool {
40
+ limits;
41
+ slots = new Map();
42
+ constructor(limits = {}) {
43
+ this.limits = limits;
44
+ }
45
+ get(key) { let slot = this.slots.get(key); if (!slot) {
46
+ slot = new AsyncSlot(this.limits[key] ?? Number.POSITIVE_INFINITY);
47
+ this.slots.set(key, slot);
48
+ } ; return slot; }
49
+ }
50
+ function toJson(value, seen = new Set()) {
51
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
52
+ return value;
53
+ if (typeof value === 'number') {
54
+ if (!Number.isFinite(value))
55
+ throw new Error('LLM_OUTPUT_NOT_SERIALIZABLE');
56
+ return value;
57
+ }
58
+ if (Array.isArray(value)) {
59
+ if (seen.has(value))
60
+ throw new Error('LLM_OUTPUT_NOT_SERIALIZABLE');
61
+ seen.add(value);
62
+ try {
63
+ return value.map((item) => toJson(item, seen));
64
+ }
65
+ finally {
66
+ seen.delete(value);
67
+ }
68
+ }
69
+ if (typeof value === 'object') {
70
+ if (value instanceof Uint8Array || value instanceof ArrayBuffer || value instanceof Date || Object.getPrototypeOf(value) !== Object.prototype || seen.has(value))
71
+ throw new Error('LLM_OUTPUT_NOT_SERIALIZABLE');
72
+ seen.add(value);
73
+ try {
74
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, toJson(item, seen)]));
75
+ }
76
+ finally {
77
+ seen.delete(value);
78
+ }
79
+ }
80
+ throw new Error('LLM_OUTPUT_NOT_SERIALIZABLE');
81
+ }
82
+ function candidateMetadata(candidate, attempts, usage, slotWaitMs, routes) {
83
+ return { selected: { id: candidate.id, providerId: candidate.providerId }, routes, attempts: attempts.map((attempt) => {
84
+ const recorded = usage.get(attempt.attemptId);
85
+ const usageJson = recorded === undefined ? undefined : { ...(recorded.inputTokens === undefined ? {} : { inputTokens: recorded.inputTokens }), ...(recorded.outputTokens === undefined ? {} : { outputTokens: recorded.outputTokens }), ...(recorded.cachedInputTokens === undefined ? {} : { cachedInputTokens: recorded.cachedInputTokens }), ...(recorded.uncachedInputTokens === undefined ? {} : { uncachedInputTokens: recorded.uncachedInputTokens }), ...(recorded.latencyMs === undefined ? {} : { latencyMs: recorded.latencyMs }), ...(recorded.cost === undefined ? {} : { cost: recorded.cost }) };
86
+ const waited = slotWaitMs.get(attempt.attemptId);
87
+ return { effectId: attempt.effectId, attemptId: attempt.attemptId, attemptNo: attempt.attemptNo, modelId: attempt.candidate.id, providerId: attempt.candidate.providerId, ...(waited === undefined ? {} : { slotWaitMs: waited }), ...(usageJson === undefined ? {} : { usage: usageJson }) };
88
+ }) };
89
+ }
90
+ export function createModelEffectExecutor(config) {
91
+ const fallback = new ModelFallbackController();
92
+ const providerSlots = new SlotPool(config.maxConcurrentByProvider);
93
+ const modelSlots = new SlotPool(config.maxConcurrentByModel);
94
+ return async (effect, signal, emitObservation) => {
95
+ if (effect.kind !== 'llm')
96
+ throw new Error(`UNSUPPORTED_EFFECT_KIND:${effect.kind}`);
97
+ const input = effect.input && typeof effect.input === 'object' && !Array.isArray(effect.input) ? effect.input : {};
98
+ const task = input.task;
99
+ const request = input.request;
100
+ if (typeof task !== 'string' || !request || typeof request !== 'object' || Array.isArray(request))
101
+ throw new Error('INVALID_LLM_EFFECT_INPUT');
102
+ const projection = request;
103
+ const observations = [];
104
+ const usage = new Map();
105
+ const slotWaitMs = new Map();
106
+ let lastSchemaViolation;
107
+ let failedForSchema = false;
108
+ const dynamicRequirements = input.requirements && typeof input.requirements === 'object' && !Array.isArray(input.requirements) ? input.requirements : {};
109
+ const structuredRequirement = dynamicRequirements.structuredOutput;
110
+ const structuredSchema = structuredRequirement && typeof structuredRequirement === 'object' && !Array.isArray(structuredRequirement) ? structuredRequirement.schema : undefined;
111
+ if (structuredSchema !== undefined && (input.outputSchema === undefined || stableSerialize(structuredSchema) !== stableSerialize(input.outputSchema)))
112
+ return { value: null, status: 'failed', executionState: 'failed', privacy: projection.privacy, error: { code: 'STRUCTURED_OUTPUT_CONTRACT_MISMATCH', message: 'requirements.structuredOutput.schema must equal outputSchema.' } };
113
+ const routeRequirements = { ...config.requirements, ...(typeof dynamicRequirements.toolCalling === 'boolean' ? { toolCalling: dynamicRequirements.toolCalling } : {}), ...(typeof dynamicRequirements.structuredOutput === 'boolean' ? { structuredOutput: dynamicRequirements.structuredOutput } : structuredSchema === undefined ? {} : { structuredOutput: true }), ...(dynamicRequirements.reasoning === 'low' || dynamicRequirements.reasoning === 'medium' || dynamicRequirements.reasoning === 'high' ? { reasoning: dynamicRequirements.reasoning } : {}), ...(typeof dynamicRequirements.maxOutputTokens === 'number' ? { maxOutputTokens: dynamicRequirements.maxOutputTokens } : {}), ...(typeof dynamicRequirements.contextSize === 'number' ? { contextSize: dynamicRequirements.contextSize } : {}) };
114
+ const routeDiagnostics = config.router.diagnostics(task, projection.privacy, routeRequirements, estimateProjectionTokens(projection) + (typeof routeRequirements.maxOutputTokens === 'number' ? routeRequirements.maxOutputTokens : 0));
115
+ const candidates = config.router.routeProjection(task, projection, routeRequirements);
116
+ const attemptNo = Math.max(1, effect.attemptNo);
117
+ const candidate = candidates[attemptNo - 1];
118
+ const maxAttempts = effect.retryPolicy?.maxAttempts ?? candidates.length;
119
+ const canFallback = candidate !== undefined && attemptNo < Math.max(0, maxAttempts) && candidates[attemptNo] !== undefined;
120
+ if (candidate === undefined)
121
+ return { value: null, status: 'failed', executionState: 'failed', privacy: projection.privacy, error: { code: candidates.length === 0 ? 'NO_ELIGIBLE_MODEL' : 'MODEL_ATTEMPT_LIMIT_REACHED', message: candidates.length === 0 ? 'No model candidate satisfies the task, privacy, capability, and context requirements.' : 'No additional routed model candidate is available for this Effect.' }, metadata: { routes: routeDiagnostics, attempts: [] } };
122
+ const result = await fallback.execute(effect.id, [candidate], async (attempt) => {
123
+ failedForSchema = false;
124
+ const provider = config.providers.get(attempt.candidate.providerId);
125
+ if (!provider)
126
+ throw modelFallbackError({ retryable: false, localClosed: true, sideEffectState: 'none', cause: new Error(`UNKNOWN_PROVIDER:${attempt.candidate.providerId}`) });
127
+ const slotStartedAt = Date.now();
128
+ let providerRelease;
129
+ try {
130
+ providerRelease = await providerSlots.get(attempt.candidate.providerId).acquire(signal);
131
+ }
132
+ catch (cause) {
133
+ if (signal.aborted)
134
+ throw modelFallbackError({ retryable: false, localClosed: true, sideEffectState: 'none', cause });
135
+ throw cause;
136
+ }
137
+ let modelRelease;
138
+ try {
139
+ modelRelease = await modelSlots.get(attempt.candidate.id).acquire(signal);
140
+ }
141
+ catch (cause) {
142
+ providerRelease();
143
+ if (signal.aborted)
144
+ throw modelFallbackError({ retryable: false, localClosed: true, sideEffectState: 'none', cause });
145
+ throw cause;
146
+ }
147
+ slotWaitMs.set(attempt.attemptId, Math.max(0, Date.now() - slotStartedAt));
148
+ const releases = [providerRelease, modelRelease];
149
+ let feedbackRecorded = false;
150
+ const recordFeedback = (outcome, quality) => {
151
+ if (feedbackRecorded)
152
+ return;
153
+ feedbackRecorded = true;
154
+ config.router.recordFeedback({ modelId: attempt.candidate.id, providerId: attempt.candidate.providerId, outcome, quality, ...(usage.get(attempt.attemptId) === undefined ? {} : { usage: usage.get(attempt.attemptId) }) });
155
+ };
156
+ try {
157
+ const startedAt = Date.now();
158
+ const onObservation = (chunk) => {
159
+ const observation = { type: 'chunk', data: chunk };
160
+ if (emitObservation)
161
+ emitObservation(observation);
162
+ else
163
+ observations.push(observation);
164
+ };
165
+ const output = assignRuntimeToolCallIds(validateAdapterResult(await provider.executeAttempt({ request: projection, signal, model: attempt.candidate.id, ...(input.outputSchema === undefined ? {} : { outputSchema: input.outputSchema }), ...(typeof routeRequirements.maxOutputTokens === 'number' ? { maxOutputTokens: routeRequirements.maxOutputTokens } : {}), onObservation })), effect.id);
166
+ const measuredUsage = output.usage === undefined ? { latencyMs: Math.max(0, Date.now() - startedAt) } : { ...output.usage, latencyMs: output.usage.latencyMs ?? Math.max(0, Date.now() - startedAt), ...(output.usage.uncachedInputTokens === undefined && output.usage.inputTokens !== undefined && output.usage.cachedInputTokens !== undefined ? { uncachedInputTokens: Math.max(0, output.usage.inputTokens - output.usage.cachedInputTokens) } : {}) };
167
+ usage.set(attempt.attemptId, measuredUsage);
168
+ if (output.finishReason === 'refusal') {
169
+ recordFeedback('refused', 0);
170
+ throw new OutputValidationError('adapter', 'MODEL_REFUSAL', output.refusal ?? 'Provider refused the request.');
171
+ }
172
+ if (input.outputSchema !== undefined) {
173
+ const candidateValue = output.structured ?? output.text;
174
+ if (!validateJsonSchema(candidateValue, input.outputSchema)) {
175
+ lastSchemaViolation = toJson(candidateValue);
176
+ failedForSchema = true;
177
+ recordFeedback('schema_rejected', 0);
178
+ throw new OutputValidationError('structured', 'OUTPUT_SCHEMA_VIOLATION', 'Provider output did not match the declared schema');
179
+ }
180
+ }
181
+ failedForSchema = false;
182
+ recordFeedback('succeeded', 1);
183
+ return output;
184
+ }
185
+ catch (cause) {
186
+ recordFeedback('failed', 0);
187
+ if (signal.aborted)
188
+ throw modelFallbackError({ retryable: false, localClosed: true, sideEffectState: 'none', cause });
189
+ const retryable = cause && typeof cause === 'object' && 'retryable' in cause && typeof cause.retryable === 'boolean' ? cause.retryable : true;
190
+ throw modelFallbackError({ retryable, localClosed: true, sideEffectState: 'none', cause });
191
+ }
192
+ finally {
193
+ for (const release of releases.reverse())
194
+ release();
195
+ }
196
+ }, 1).then((value) => {
197
+ const providerAttempt = value.attempts.at(-1);
198
+ const measuredUsage = providerAttempt === undefined ? undefined : usage.get(providerAttempt.attemptId);
199
+ if (measuredUsage !== undefined)
200
+ usage.set(effect.attemptId, measuredUsage);
201
+ const waited = providerAttempt === undefined ? undefined : slotWaitMs.get(providerAttempt.attemptId);
202
+ if (waited !== undefined)
203
+ slotWaitMs.set(effect.attemptId, waited);
204
+ return { ...value, attempts: value.attempts.map(() => ({ effectId: effect.id, attemptId: effect.attemptId, attemptNo, candidate })) };
205
+ }).catch((cause) => {
206
+ if (failedForSchema && lastSchemaViolation !== undefined)
207
+ return { result: { text: '', toolCalls: [], finishReason: 'error' }, candidate, attempts: [{ effectId: effect.id, attemptId: effect.attemptId, attemptNo, candidate }], schemaRejected: lastSchemaViolation };
208
+ const inner = cause && typeof cause === 'object' && 'modelFallback' in cause ? cause.modelFallback?.cause : cause;
209
+ if (inner instanceof OutputValidationError && inner.code === 'MODEL_REFUSAL')
210
+ return { result: { text: '', refusal: inner.message, toolCalls: [], finishReason: 'refusal' }, candidate, attempts: [{ effectId: effect.id, attemptId: effect.attemptId, attemptNo, candidate }], refused: true, retryable: canFallback };
211
+ const fallbackError = cause && typeof cause === 'object' && 'modelFallback' in cause ? cause.modelFallback : undefined;
212
+ const error = runtimeErrorFromCause(fallbackError?.cause ?? cause, 'MODEL_EXECUTION_FAILED');
213
+ return { result: { text: '', toolCalls: [], finishReason: 'error' }, candidate, attempts: [{ effectId: effect.id, attemptId: effect.attemptId, attemptNo, candidate }], failed: { ...error, ...(canFallback && fallbackError?.retryable !== false && error.retryable !== false ? { retryable: true } : {}) } };
214
+ });
215
+ if ('schemaRejected' in result)
216
+ return { value: null, status: 'failed', executionState: 'failed', privacy: projection.privacy, error: { code: 'OUTPUT_SCHEMA_VIOLATION', message: 'Provider output did not match the declared schema.', ...(canFallback ? { retryable: true } : {}) }, metadata: candidateMetadata(result.candidate, result.attempts, usage, slotWaitMs, routeDiagnostics), rejectedOutput: { value: result.schemaRejected, privacy: projection.privacy, derivedFrom: [...(effect.derivedFrom ?? [])] } };
217
+ if ('refused' in result)
218
+ return { value: null, status: 'failed', executionState: 'failed', privacy: projection.privacy, error: { code: 'MODEL_REFUSAL', message: result.result.refusal ?? 'Provider refused the request.', ...(result.retryable ? { retryable: true } : {}) }, metadata: candidateMetadata(result.candidate, result.attempts, usage, slotWaitMs, routeDiagnostics) };
219
+ if ('failed' in result)
220
+ return { value: null, status: 'failed', executionState: 'failed', privacy: projection.privacy, error: result.failed, metadata: candidateMetadata(result.candidate, result.attempts, usage, slotWaitMs, routeDiagnostics) };
221
+ const modelValue = input.outputSchema !== undefined || typeof input.schema === 'string' ? (result.result.structured ?? result.result.text) : result.result;
222
+ const value = toJson(modelValue);
223
+ return { value, privacy: projection.privacy, sideEffectState: 'none', executionState: 'succeeded', metadata: candidateMetadata(result.candidate, result.attempts, usage, slotWaitMs, routeDiagnostics), ...(observations.length ? { observations } : {}) };
224
+ };
225
+ }
@@ -0,0 +1,28 @@
1
+ import type { JsonValue, LLMRequestProjection, LLMResult } from '@hunterzhu/pulse-runtime';
2
+ export type ProviderToolChoice = 'auto' | 'required' | 'none' | {
3
+ type: 'function';
4
+ function: {
5
+ name: string;
6
+ };
7
+ };
8
+ export interface ProviderAdapter {
9
+ readonly id: string;
10
+ readonly name: string;
11
+ executeAttempt(params: {
12
+ request: LLMRequestProjection;
13
+ signal: AbortSignal;
14
+ onObservation?: (chunk: string) => void;
15
+ model?: string;
16
+ outputSchema?: JsonValue;
17
+ maxOutputTokens?: number;
18
+ }): Promise<LLMResult>;
19
+ }
20
+ export interface ProviderPresetConfig {
21
+ provider: string;
22
+ apiKey?: string;
23
+ baseURL?: string;
24
+ defaultModel?: string;
25
+ maxOutputTokens?: number;
26
+ toolChoice?: ProviderToolChoice;
27
+ extraHeaders?: Record<string, string>;
28
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ export interface FilesystemWriteResult {
2
+ hash: string;
3
+ bytes: number;
4
+ }
5
+ export interface FilesystemReadResult {
6
+ content: string;
7
+ truncated: boolean;
8
+ }
9
+ export declare class FilesystemTool {
10
+ readonly root: string;
11
+ private readonly lockTimeoutMs;
12
+ constructor(root: string, lockTimeoutMs?: number);
13
+ private safe;
14
+ private existing;
15
+ private writable;
16
+ read(path: string, signal?: AbortSignal): Promise<string>;
17
+ readLimited(path: string, maxBytes: number, signal?: AbortSignal): Promise<FilesystemReadResult>;
18
+ list(path?: string, signal?: AbortSignal): Promise<string[]>;
19
+ write(path: string, content: string, signal?: AbortSignal): Promise<void>;
20
+ move(source: string, destination: string, expectedHash?: string, signal?: AbortSignal): Promise<{
21
+ hash: string;
22
+ bytes: number;
23
+ }>;
24
+ hash(path: string, signal?: AbortSignal): Promise<string>;
25
+ writeIfUnchanged(path: string, content: string, expectedHash: string, signal?: AbortSignal): Promise<FilesystemWriteResult>;
26
+ private withLock;
27
+ }
@@ -0,0 +1,183 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { createReadStream } from 'node:fs';
3
+ import { lstat, mkdir, open, readFile, readdir, realpath, rename, rm, writeFile } from 'node:fs/promises';
4
+ import { dirname, isAbsolute, relative, resolve } from 'node:path';
5
+ import { pipeline } from 'node:stream/promises';
6
+ function filesystemError(code, retryable = false, cause) {
7
+ return Object.assign(new Error(code), { code, retryable, ...(cause === undefined ? {} : { cause }) });
8
+ }
9
+ function filesystemCause(cause) {
10
+ if (cause instanceof Error && typeof cause.code === 'string' && typeof cause.retryable === 'boolean')
11
+ return cause;
12
+ const code = cause && typeof cause === 'object' && typeof cause.code === 'string' ? String(cause.code) : 'FILESYSTEM_OPERATION_FAILED';
13
+ const retryable = code === 'EAGAIN' || code === 'EBUSY' || code === 'EMFILE' || code === 'ENFILE' || code === 'ETIMEDOUT';
14
+ return filesystemError(code, retryable, cause);
15
+ }
16
+ function pidAlive(pid) {
17
+ try {
18
+ process.kill(pid, 0);
19
+ return true;
20
+ }
21
+ catch (cause) {
22
+ if (cause.code === 'ESRCH')
23
+ return false;
24
+ throw cause;
25
+ }
26
+ }
27
+ export class FilesystemTool {
28
+ root;
29
+ lockTimeoutMs;
30
+ constructor(root, lockTimeoutMs = 30_000) {
31
+ this.root = root;
32
+ this.lockTimeoutMs = lockTimeoutMs;
33
+ }
34
+ safe(path) { const target = resolve(this.root, path); if (isAbsolute(path) || relative(resolve(this.root), target).startsWith('..'))
35
+ throw filesystemError('PATH_OUTSIDE_SANDBOX'); return target; }
36
+ async existing(path) {
37
+ const target = this.safe(path);
38
+ const [root, resolved] = await Promise.all([realpath(this.root), realpath(target)]);
39
+ const within = relative(root, resolved);
40
+ if (within.startsWith('..') || isAbsolute(within))
41
+ throw filesystemError('PATH_OUTSIDE_SANDBOX');
42
+ return resolved;
43
+ }
44
+ async writable(path) {
45
+ const target = this.safe(path);
46
+ await mkdir(dirname(target), { recursive: true });
47
+ const [root, parent] = await Promise.all([realpath(this.root), realpath(dirname(target))]);
48
+ const within = relative(root, parent);
49
+ if (within.startsWith('..') || isAbsolute(within))
50
+ throw filesystemError('PATH_OUTSIDE_SANDBOX');
51
+ const existing = await lstat(target).catch((cause) => cause.code === 'ENOENT' ? undefined : Promise.reject(cause));
52
+ if (existing?.isSymbolicLink())
53
+ throw filesystemError('PATH_OUTSIDE_SANDBOX');
54
+ return target;
55
+ }
56
+ async read(path, signal) { if (signal?.aborted)
57
+ throw filesystemError('ABORTED'); return readFile(await this.existing(path), 'utf8'); }
58
+ async readLimited(path, maxBytes, signal) {
59
+ if (signal?.aborted)
60
+ throw filesystemError('ABORTED');
61
+ const handle = await open(await this.existing(path), 'r');
62
+ try {
63
+ const buffer = Buffer.alloc(maxBytes + 1);
64
+ const { bytesRead } = await handle.read(buffer, 0, maxBytes + 1, 0);
65
+ return { content: buffer.subarray(0, Math.min(bytesRead, maxBytes)).toString('utf8'), truncated: bytesRead > maxBytes };
66
+ }
67
+ finally {
68
+ await handle.close();
69
+ }
70
+ }
71
+ async list(path = '.', signal) { if (signal?.aborted)
72
+ throw filesystemError('ABORTED'); return readdir(await this.existing(path)); }
73
+ async write(path, content, signal) { if (signal?.aborted)
74
+ throw filesystemError('ABORTED'); await writeFile(await this.writable(path), content, 'utf8'); }
75
+ async move(source, destination, expectedHash, signal) {
76
+ if (signal?.aborted)
77
+ throw filesystemError('ABORTED');
78
+ const sourcePath = this.safe(source);
79
+ const sourceEntry = await lstat(sourcePath).catch((cause) => cause.code === 'ENOENT' ? undefined : Promise.reject(cause));
80
+ if (!sourceEntry)
81
+ throw filesystemError('ENOENT');
82
+ if (sourceEntry.isSymbolicLink())
83
+ throw filesystemError('PATH_OUTSIDE_SANDBOX');
84
+ const sourceTarget = await this.existing(source);
85
+ const destinationTarget = await this.writable(destination);
86
+ const destinationExists = await lstat(destinationTarget).catch((cause) => cause.code === 'ENOENT' ? undefined : Promise.reject(cause));
87
+ if (destinationExists)
88
+ throw filesystemError('MOVE_DESTINATION_EXISTS');
89
+ const currentHash = await this.hash(source, signal);
90
+ if (expectedHash !== undefined && currentHash !== expectedHash)
91
+ throw filesystemError('FILE_BASELINE_CONFLICT');
92
+ await this.withLock(sourceTarget, async () => { await rename(sourceTarget, destinationTarget); });
93
+ return { hash: currentHash, bytes: sourceEntry.size };
94
+ }
95
+ async hash(path, signal) {
96
+ if (signal?.aborted)
97
+ throw filesystemError('ABORTED');
98
+ const digest = createHash('sha256');
99
+ await pipeline(createReadStream(await this.existing(path)), digest, signal === undefined ? {} : { signal });
100
+ return digest.digest('hex');
101
+ }
102
+ async writeIfUnchanged(path, content, expectedHash, signal) {
103
+ if (signal?.aborted)
104
+ throw filesystemError('ABORTED');
105
+ if (!/^[a-f0-9]{64}$/.test(expectedHash))
106
+ throw filesystemError('INVALID_FILE_BASELINE_HASH');
107
+ const target = await this.writable(path);
108
+ return this.withLock(target, async () => {
109
+ if (signal?.aborted)
110
+ throw filesystemError('ABORTED');
111
+ let current;
112
+ try {
113
+ current = await readFile(target);
114
+ }
115
+ catch (cause) {
116
+ if (cause.code === 'ENOENT')
117
+ throw filesystemError('FILE_BASELINE_MISSING');
118
+ throw filesystemCause(cause);
119
+ }
120
+ const currentHash = createHash('sha256').update(current).digest('hex');
121
+ if (currentHash !== expectedHash)
122
+ throw filesystemError('FILE_BASELINE_CONFLICT');
123
+ const bytes = Buffer.byteLength(content, 'utf8');
124
+ const temporary = `${target}.tmp-${process.pid}-${process.hrtime.bigint().toString()}`;
125
+ let handle;
126
+ try {
127
+ handle = await open(temporary, 'wx', 0o600);
128
+ await handle.writeFile(content, 'utf8');
129
+ await handle.sync();
130
+ await handle.close();
131
+ handle = undefined;
132
+ await rename(temporary, target);
133
+ }
134
+ finally {
135
+ if (handle)
136
+ await handle.close().catch(() => undefined);
137
+ await rm(temporary, { force: true }).catch(() => undefined);
138
+ }
139
+ return { hash: createHash('sha256').update(content).digest('hex'), bytes };
140
+ });
141
+ }
142
+ async withLock(target, work) {
143
+ const lockPath = `${target}.pulse.lock`;
144
+ const deadline = Date.now() + this.lockTimeoutMs;
145
+ const payload = JSON.stringify({ pid: process.pid, token: `${process.pid}:${process.hrtime.bigint()}` });
146
+ let lock;
147
+ while (lock === undefined) {
148
+ try {
149
+ lock = await open(lockPath, 'wx', 0o600);
150
+ await lock.writeFile(payload);
151
+ }
152
+ catch (cause) {
153
+ if (cause.code !== 'EEXIST')
154
+ throw filesystemCause(cause);
155
+ const body = await readFile(lockPath, 'utf8').catch(() => undefined);
156
+ let owner;
157
+ try {
158
+ owner = body ? JSON.parse(body) : undefined;
159
+ }
160
+ catch {
161
+ owner = undefined;
162
+ }
163
+ if (typeof owner?.pid === 'number' && !pidAlive(owner.pid) && body !== undefined) {
164
+ const current = await readFile(lockPath, 'utf8').catch(() => undefined);
165
+ if (current === body) {
166
+ await rm(lockPath, { force: true });
167
+ continue;
168
+ }
169
+ }
170
+ if (Date.now() >= deadline)
171
+ throw filesystemError('FILESYSTEM_LOCK_TIMEOUT', true);
172
+ await new Promise((resolve) => setTimeout(resolve, 5));
173
+ }
174
+ }
175
+ try {
176
+ return await work();
177
+ }
178
+ finally {
179
+ await lock.close().catch(() => undefined);
180
+ await rm(lockPath, { force: true }).catch(() => undefined);
181
+ }
182
+ }
183
+ }
@@ -0,0 +1,5 @@
1
+ import { type EffectExecutor, type EffectRecord, type EffectSubmission, type JsonValue } from '@hunterzhu/pulse-runtime';
2
+ import { ToolRegistry, type ReconcileResult } from '@hunterzhu/pulse-tool-sdk';
3
+ export declare function createToolEffectExecutor(registry: ToolRegistry): EffectExecutor;
4
+ export declare function reconcileToolEffect(registry: ToolRegistry, effect: Readonly<EffectRecord>, signal: AbortSignal): Promise<ReconcileResult<JsonValue>>;
5
+ export declare function createToolEffectSubmissionPreparer(registry: ToolRegistry): (submission: EffectSubmission) => EffectSubmission;
@@ -0,0 +1,141 @@
1
+ import { isSideEffectful } from '@hunterzhu/pulse-runtime';
2
+ function toJson(value, seen = new Set()) {
3
+ if (value === null || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number')
4
+ return value;
5
+ if (Array.isArray(value)) {
6
+ if (seen.has(value))
7
+ throw new Error('TOOL_OUTPUT_NOT_SERIALIZABLE');
8
+ seen.add(value);
9
+ try {
10
+ return value.map((item) => toJson(item, seen));
11
+ }
12
+ finally {
13
+ seen.delete(value);
14
+ }
15
+ }
16
+ if (typeof value === 'object') {
17
+ if (value instanceof Uint8Array || value instanceof ArrayBuffer || value instanceof Date || Object.getPrototypeOf(value) !== Object.prototype)
18
+ throw new Error('TOOL_OUTPUT_NOT_SERIALIZABLE');
19
+ if (seen.has(value))
20
+ throw new Error('TOOL_OUTPUT_NOT_SERIALIZABLE');
21
+ seen.add(value);
22
+ try {
23
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, toJson(item, seen)]));
24
+ }
25
+ finally {
26
+ seen.delete(value);
27
+ }
28
+ }
29
+ throw new Error('TOOL_OUTPUT_NOT_SERIALIZABLE');
30
+ }
31
+ function artifactOutput(value) {
32
+ if (value instanceof Uint8Array)
33
+ return { mediaType: 'application/octet-stream', content: new Uint8Array(value) };
34
+ if (value instanceof ArrayBuffer)
35
+ return { mediaType: 'application/octet-stream', content: new Uint8Array(value) };
36
+ try {
37
+ const serialized = JSON.stringify(value);
38
+ if (serialized !== undefined)
39
+ return { mediaType: 'application/json', content: serialized };
40
+ }
41
+ catch { /* fall through to a bounded textual representation */ }
42
+ return { mediaType: 'text/plain', content: String(value) };
43
+ }
44
+ export function createToolEffectExecutor(registry) {
45
+ return async (effect, signal, emitObservation) => {
46
+ if (effect.kind !== 'tool')
47
+ throw new Error(`UNSUPPORTED_EFFECT_KIND:${effect.kind}`);
48
+ const input = effect.input && typeof effect.input === 'object' && !Array.isArray(effect.input) ? effect.input : {};
49
+ const name = input.name;
50
+ if (typeof name !== 'string')
51
+ throw new Error('INVALID_TOOL_EFFECT_INPUT');
52
+ if (!registry.isAllowed(name))
53
+ throw new Error(`TOOL_NOT_ALLOWED:${name}`);
54
+ const definition = registry.get(name);
55
+ if (!definition)
56
+ throw new Error(`UNKNOWN_TOOL:${name}`);
57
+ const observations = [];
58
+ const emit = (event) => {
59
+ if (signal.aborted)
60
+ return;
61
+ if (emitObservation)
62
+ emitObservation(event);
63
+ else
64
+ observations.push(event);
65
+ };
66
+ const toolContext = {
67
+ toolCallId: effect.toolCallId ?? '',
68
+ effectId: effect.id,
69
+ attemptId: effect.attemptId,
70
+ ...(effect.idempotencyKey === undefined ? {} : { idempotencyKey: effect.idempotencyKey }),
71
+ agentId: effect.agentId,
72
+ laneId: effect.ownerLaneId,
73
+ signal,
74
+ emit,
75
+ };
76
+ const executionRef = registry.executionRef(name, input.arguments ?? {}, toolContext);
77
+ let detailed;
78
+ try {
79
+ detailed = await registry.executeDetailed(name, input.arguments ?? {}, {
80
+ toolCallId: effect.toolCallId ?? '',
81
+ effectId: effect.id,
82
+ attemptId: effect.attemptId,
83
+ ...(effect.idempotencyKey === undefined ? {} : { idempotencyKey: effect.idempotencyKey }),
84
+ agentId: effect.agentId,
85
+ laneId: effect.ownerLaneId,
86
+ signal,
87
+ emit,
88
+ });
89
+ }
90
+ catch (error) {
91
+ if (signal.aborted && isSideEffectful(definition.manifest.sideEffectPolicy))
92
+ return { value: null, executionState: 'remote_unknown', sideEffectState: 'unknown', ...(executionRef === undefined ? {} : { executionRef }), metadata: { toolVersion: definition.manifest.version, reconcileRequired: true }, ...(error instanceof Error ? { error: { code: 'TOOL_CANCELLED_UNKNOWN', message: error.message } } : {}) };
93
+ throw error;
94
+ }
95
+ const summary = detailed.summary === undefined ? undefined : toJson(detailed.summary);
96
+ let value;
97
+ let artifact;
98
+ try {
99
+ value = toJson(detailed.output);
100
+ }
101
+ catch {
102
+ value = null;
103
+ artifact = artifactOutput(detailed.output);
104
+ }
105
+ return { value, ...(detailed.normalized === undefined ? {} : { normalized: toJson(detailed.normalized) }), ...(artifact === undefined ? {} : { artifact }), ...(summary === undefined ? {} : { summary }), sideEffectState: isSideEffectful(definition.manifest.sideEffectPolicy) ? 'applied' : 'none', executionState: 'succeeded', ...(executionRef === undefined ? {} : { executionRef }), metadata: { toolVersion: detailed.manifest.version, retrySafety: detailed.manifest.retrySafety, defaultTimeoutMs: detailed.manifest.defaultTimeoutMs, observationCount: observations.length, ...(artifact === undefined ? {} : { artifactMediaType: artifact.mediaType }) }, ...(observations.length ? { observations } : {}) };
106
+ };
107
+ }
108
+ export async function reconcileToolEffect(registry, effect, signal) {
109
+ if (effect.kind !== 'tool')
110
+ throw new Error(`UNSUPPORTED_EFFECT_KIND:${effect.kind}`);
111
+ if (effect.executionRef === undefined)
112
+ throw new Error('MISSING_TOOL_EXECUTION_REF');
113
+ const input = effect.input && typeof effect.input === 'object' && !Array.isArray(effect.input) ? effect.input : {};
114
+ if (typeof input.name !== 'string')
115
+ throw new Error('INVALID_TOOL_EFFECT_INPUT');
116
+ const result = await registry.reconcileDetailed(input.name, effect.executionRef, { toolCallId: effect.toolCallId ?? '', effectId: effect.id, attemptId: effect.attemptId, agentId: effect.agentId, laneId: effect.ownerLaneId, signal });
117
+ return { status: result.status, ...(result.error === undefined ? {} : { error: result.error }), ...(result.output === undefined ? {} : { output: toJson(result.output) }) };
118
+ }
119
+ export function createToolEffectSubmissionPreparer(registry) {
120
+ return (submission) => {
121
+ if (submission.kind === 'llm') {
122
+ const input = submission.input && typeof submission.input === 'object' && !Array.isArray(submission.input) ? submission.input : {};
123
+ const rawQuery = input.toolDiscovery;
124
+ if (rawQuery && typeof rawQuery === 'object' && !Array.isArray(rawQuery)) {
125
+ const query = rawQuery;
126
+ const requestedId = typeof input.toolSetId === 'string' ? input.toolSetId : 'dynamic';
127
+ const toolSet = registry.compileToolSet(requestedId, query);
128
+ const tools = toolSet.tools.map((manifest) => ({ name: manifest.name, description: manifest.description, inputSchema: manifest.inputSchema }));
129
+ return { ...submission, input: { ...input, toolSetId: `${toolSet.id}@${toolSet.version}`, tools: { tools } } };
130
+ }
131
+ return submission;
132
+ }
133
+ if (submission.kind !== 'tool')
134
+ return submission;
135
+ const input = submission.input && typeof submission.input === 'object' && !Array.isArray(submission.input) ? submission.input : {};
136
+ if (typeof input.name !== 'string')
137
+ throw new Error('INVALID_TOOL_EFFECT_INPUT');
138
+ const admission = registry.admission(input.name, input.arguments ?? {});
139
+ return { ...submission, ...(submission.locks === undefined ? { locks: admission.locks } : {}), ...(submission.sideEffectPolicy === undefined ? { sideEffectPolicy: admission.sideEffectPolicy } : {}), ...(submission.attemptTimeoutMs === undefined ? { attemptTimeoutMs: admission.defaultTimeoutMs } : {}), ...(submission.toolVersion === undefined ? { toolVersion: admission.version } : {}) };
140
+ };
141
+ }
@@ -0,0 +1,15 @@
1
+ export interface ShellResult {
2
+ code: number | null;
3
+ stdout: string;
4
+ stderr: string;
5
+ truncated: boolean;
6
+ timedOut: boolean;
7
+ aborted: boolean;
8
+ }
9
+ export declare function runShell(command: string, args?: string[], options?: {
10
+ cwd?: string;
11
+ signal?: AbortSignal;
12
+ timeoutMs?: number;
13
+ maxOutputBytes?: number;
14
+ env?: NodeJS.ProcessEnv;
15
+ }): Promise<ShellResult>;