@sapiom/agent-core 0.6.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/CHANGELOG.md +246 -0
- package/LICENSE +21 -0
- package/dist/cjs/__tests__/execution-detail.wire.fixture.json +101 -0
- package/dist/cjs/__tests__/execution-projection.preseam.fixture.json +37 -0
- package/dist/cjs/__tests__/execution-projection.with-cost.fixture.json +60 -0
- package/dist/cjs/bundle.d.ts +5 -0
- package/dist/cjs/bundle.js +134 -0
- package/dist/cjs/check.d.ts +10 -0
- package/dist/cjs/check.js +154 -0
- package/dist/cjs/client.d.ts +21 -0
- package/dist/cjs/client.js +108 -0
- package/dist/cjs/clone.d.ts +17 -0
- package/dist/cjs/clone.js +71 -0
- package/dist/cjs/config.d.ts +16 -0
- package/dist/cjs/config.js +40 -0
- package/dist/cjs/decode.d.ts +4 -0
- package/dist/cjs/decode.js +172 -0
- package/dist/cjs/deploy.d.ts +12 -0
- package/dist/cjs/deploy.js +67 -0
- package/dist/cjs/errors.d.ts +15 -0
- package/dist/cjs/errors.js +23 -0
- package/dist/cjs/git.d.ts +12 -0
- package/dist/cjs/git.js +76 -0
- package/dist/cjs/index.d.ts +41 -0
- package/dist/cjs/index.js +72 -0
- package/dist/cjs/inspect.d.ts +40 -0
- package/dist/cjs/inspect.js +99 -0
- package/dist/cjs/link.d.ts +15 -0
- package/dist/cjs/link.js +19 -0
- package/dist/cjs/local/dispatcher.d.ts +37 -0
- package/dist/cjs/local/dispatcher.js +149 -0
- package/dist/cjs/local/load.d.ts +6 -0
- package/dist/cjs/local/load.js +102 -0
- package/dist/cjs/local/run-local.d.ts +32 -0
- package/dist/cjs/local/run-local.js +95 -0
- package/dist/cjs/local/stubs.d.ts +8 -0
- package/dist/cjs/local/stubs.js +35 -0
- package/dist/cjs/run.d.ts +11 -0
- package/dist/cjs/run.js +29 -0
- package/dist/cjs/scaffold.d.ts +24 -0
- package/dist/cjs/scaffold.js +190 -0
- package/dist/cjs/schedule.d.ts +67 -0
- package/dist/cjs/schedule.js +33 -0
- package/dist/cjs/signal.d.ts +12 -0
- package/dist/cjs/signal.js +24 -0
- package/dist/cjs/types.d.ts +99 -0
- package/dist/cjs/types.js +11 -0
- package/dist/cjs/watch.d.ts +10 -0
- package/dist/cjs/watch.js +101 -0
- package/dist/esm/__tests__/execution-detail.wire.fixture.json +101 -0
- package/dist/esm/__tests__/execution-projection.preseam.fixture.json +37 -0
- package/dist/esm/__tests__/execution-projection.with-cost.fixture.json +60 -0
- package/dist/esm/bundle.d.ts +5 -0
- package/dist/esm/bundle.js +95 -0
- package/dist/esm/check.d.ts +10 -0
- package/dist/esm/check.js +115 -0
- package/dist/esm/client.d.ts +21 -0
- package/dist/esm/client.js +103 -0
- package/dist/esm/clone.d.ts +17 -0
- package/dist/esm/clone.js +65 -0
- package/dist/esm/config.d.ts +16 -0
- package/dist/esm/config.js +31 -0
- package/dist/esm/decode.d.ts +4 -0
- package/dist/esm/decode.js +167 -0
- package/dist/esm/deploy.d.ts +12 -0
- package/dist/esm/deploy.js +61 -0
- package/dist/esm/errors.d.ts +15 -0
- package/dist/esm/errors.js +19 -0
- package/dist/esm/git.d.ts +12 -0
- package/dist/esm/git.js +69 -0
- package/dist/esm/index.d.ts +41 -0
- package/dist/esm/index.js +21 -0
- package/dist/esm/inspect.d.ts +40 -0
- package/dist/esm/inspect.js +92 -0
- package/dist/esm/link.d.ts +15 -0
- package/dist/esm/link.js +16 -0
- package/dist/esm/local/dispatcher.d.ts +37 -0
- package/dist/esm/local/dispatcher.js +145 -0
- package/dist/esm/local/load.d.ts +6 -0
- package/dist/esm/local/load.js +63 -0
- package/dist/esm/local/run-local.d.ts +32 -0
- package/dist/esm/local/run-local.js +87 -0
- package/dist/esm/local/stubs.d.ts +8 -0
- package/dist/esm/local/stubs.js +31 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/run.d.ts +11 -0
- package/dist/esm/run.js +25 -0
- package/dist/esm/scaffold.d.ts +24 -0
- package/dist/esm/scaffold.js +179 -0
- package/dist/esm/schedule.d.ts +67 -0
- package/dist/esm/schedule.js +26 -0
- package/dist/esm/signal.d.ts +12 -0
- package/dist/esm/signal.js +20 -0
- package/dist/esm/types.d.ts +99 -0
- package/dist/esm/types.js +8 -0
- package/dist/esm/watch.d.ts +10 -0
- package/dist/esm/watch.js +96 -0
- package/dist/tsconfig.cjs.tsbuildinfo +1 -0
- package/dist/tsconfig.esm.tsbuildinfo +1 -0
- package/package.json +73 -0
- package/templates/coding-pause/AGENTS.md +48 -0
- package/templates/coding-pause/CLAUDE.md +7 -0
- package/templates/coding-pause/README.md +53 -0
- package/templates/coding-pause/_gitignore +3 -0
- package/templates/coding-pause/index.ts +109 -0
- package/templates/coding-pause/package.json +21 -0
- package/templates/coding-pause/tsconfig.json +12 -0
- package/templates/default/AGENTS.md +48 -0
- package/templates/default/CLAUDE.md +7 -0
- package/templates/default/README.md +30 -0
- package/templates/default/_gitignore +3 -0
- package/templates/default/index.ts +26 -0
- package/templates/default/package.json +21 -0
- package/templates/default/tsconfig.json +12 -0
package/dist/esm/git.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { AgentOperationError } from './errors.js';
|
|
3
|
+
function git(args, cwd) {
|
|
4
|
+
try {
|
|
5
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
6
|
+
}
|
|
7
|
+
catch (err) {
|
|
8
|
+
const stderr = err.stderr?.toString().trim();
|
|
9
|
+
throw new AgentOperationError({
|
|
10
|
+
code: 'GIT',
|
|
11
|
+
message: `git ${args[0]} failed.`,
|
|
12
|
+
hint: stderr || (err instanceof Error ? err.message : String(err)),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function assertDeployable(dir) {
|
|
17
|
+
try {
|
|
18
|
+
execFileSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: dir, stdio: 'ignore' });
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new AgentOperationError({
|
|
22
|
+
code: 'NOT_GIT',
|
|
23
|
+
message: 'Not a git repository.',
|
|
24
|
+
hint: 'Initialize one: git init && git add -A && git commit -m "init"',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
execFileSync('git', ['rev-parse', 'HEAD'], { cwd: dir, stdio: 'ignore' });
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
throw new AgentOperationError({
|
|
32
|
+
code: 'NO_COMMITS',
|
|
33
|
+
message: 'This repository has no commits yet.',
|
|
34
|
+
hint: 'Commit your work: git add -A && git commit -m "…"',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function redactCredentials(text) {
|
|
39
|
+
return text.replace(/(https?:\/\/)[^@\s/]+@/gi, '$1***@');
|
|
40
|
+
}
|
|
41
|
+
export function cloneRepo(opts) {
|
|
42
|
+
const { cloneUrl, targetDir, branch, repoFullName, cwd } = opts;
|
|
43
|
+
try {
|
|
44
|
+
execFileSync('git', ['clone', '--depth', '1', '--single-branch', '--branch', branch, '--', cloneUrl, targetDir], { cwd, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' });
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
const stderr = err.stderr?.toString();
|
|
48
|
+
const raw = stderr?.trim() || (err instanceof Error ? err.message : String(err));
|
|
49
|
+
throw new AgentOperationError({
|
|
50
|
+
code: 'GIT_CLONE',
|
|
51
|
+
message: 'git clone failed.',
|
|
52
|
+
hint: redactCredentials(raw),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
git(['remote', 'set-url', 'origin', `https://github.com/${repoFullName}.git`], targetDir);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function pushHead(dir, pushUrl, branch) {
|
|
62
|
+
git(['push', '--force', pushUrl, `HEAD:${branch}`], dir);
|
|
63
|
+
}
|
|
64
|
+
export function pushSynthesizedTree(treeDir, pushUrl, branch) {
|
|
65
|
+
git(['init', '-q', '-b', branch], treeDir);
|
|
66
|
+
git(['add', '-A'], treeDir);
|
|
67
|
+
git(['-c', 'user.email=deploy@sapiom.ai', '-c', 'user.name=Sapiom Deploy', 'commit', '-q', '-m', 'deploy'], treeDir);
|
|
68
|
+
git(['push', '--force', pushUrl, `HEAD:${branch}`], treeDir);
|
|
69
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export { AgentOperationError } from "./errors.js";
|
|
2
|
+
export type { StructuredError } from "./errors.js";
|
|
3
|
+
export { GatewayClient, createClient, DEFAULT_WORKFLOWS_HOST, } from "./client.js";
|
|
4
|
+
export type { ClientOptions, GatewayErrorBody } from "./client.js";
|
|
5
|
+
export { readConfig, requireConfig, writeConfig, CONFIG_FILE, } from "./config.js";
|
|
6
|
+
export type { SapiomConfig, LinkedSapiomConfig } from "./config.js";
|
|
7
|
+
export { scaffold, resolveVersions, resolveTemplate, listTemplates, DEFAULT_TEMPLATE, } from "./scaffold.js";
|
|
8
|
+
export type { ScaffoldOptions, ScaffoldResult, ResolvedVersions, } from "./scaffold.js";
|
|
9
|
+
export { check } from "./check.js";
|
|
10
|
+
export type { CheckOptions, CheckResult } from "./check.js";
|
|
11
|
+
export { bundleForDeploy } from "./bundle.js";
|
|
12
|
+
export type { DeployBundle } from "./bundle.js";
|
|
13
|
+
export { link } from "./link.js";
|
|
14
|
+
export type { LinkOptions, LinkResult, DefinitionSummary } from "./link.js";
|
|
15
|
+
export { clone } from "./clone.js";
|
|
16
|
+
export type { CloneOptions, CloneResult } from "./clone.js";
|
|
17
|
+
export { deploy } from "./deploy.js";
|
|
18
|
+
export type { DeployOptions, DeployResult } from "./deploy.js";
|
|
19
|
+
export { run, parseJsonInput } from "./run.js";
|
|
20
|
+
export type { RunOptions, RunResult } from "./run.js";
|
|
21
|
+
export type { ExecutionProjection, StepProjection, CostNode, SettleState, ExecutionRef, DispatchRef, StepError, StepErrorTrace, StepErrorFrame, StepEvent, SseEvent, SseEventType, } from "./types.js";
|
|
22
|
+
export { SSE_EVENT_TYPES } from "./types.js";
|
|
23
|
+
export { decodeExecutionProjection } from "./decode.js";
|
|
24
|
+
export { inspect, listExecutions, inspectBuild, waitForExecution, isExecutionTerminal, } from "./inspect.js";
|
|
25
|
+
export type { InspectOptions, InspectBuildOptions, InspectBuildResult, BuildDetail, WaitForExecutionOptions, WaitForExecutionResult, WaitStopReason, } from "./inspect.js";
|
|
26
|
+
export { watchExecution, parseSseFrame, parseSseEvent } from "./watch.js";
|
|
27
|
+
export type { WatchExecutionOptions } from "./watch.js";
|
|
28
|
+
export { signal, parseSignalPayload } from "./signal.js";
|
|
29
|
+
export type { SignalOptions, SignalResult } from "./signal.js";
|
|
30
|
+
export { createSchedule, listSchedules, getSchedule, cancelSchedule, previewCron } from "./schedule.js";
|
|
31
|
+
export type { ScheduleKind, ScheduleStatus, SchedulePolicy, CreateScheduleOptions, ListSchedulesOptions, CronPreviewOptions, CronPreview, ScheduleSummary, ScheduleDetail, ScheduleFireRecord, } from "./schedule.js";
|
|
32
|
+
export { assertDeployable, pushHead, cloneRepo, redactCredentials } from "./git.js";
|
|
33
|
+
export type { CloneRepoOptions } from "./git.js";
|
|
34
|
+
export { parseStubFile, STUB_FILE_VERSION } from "./local/stubs.js";
|
|
35
|
+
export type { StubFile, StepStubs, StubResponse } from "./local/stubs.js";
|
|
36
|
+
export { runLocal, runLocalFromDir, STUBS_FILE } from "./local/run-local.js";
|
|
37
|
+
export type { RunLocalOptions, LocalRunResult, LocalRunOutcome, } from "./local/run-local.js";
|
|
38
|
+
export { loadDefinition } from "./local/load.js";
|
|
39
|
+
export type { LoadedDefinition } from "./local/load.js";
|
|
40
|
+
export { LocalStubDispatcher } from "./local/dispatcher.js";
|
|
41
|
+
export type { LocalStepTrace, LogEntry } from "./local/dispatcher.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export { AgentOperationError } from "./errors.js";
|
|
2
|
+
export { GatewayClient, createClient, DEFAULT_WORKFLOWS_HOST, } from "./client.js";
|
|
3
|
+
export { readConfig, requireConfig, writeConfig, CONFIG_FILE, } from "./config.js";
|
|
4
|
+
export { scaffold, resolveVersions, resolveTemplate, listTemplates, DEFAULT_TEMPLATE, } from "./scaffold.js";
|
|
5
|
+
export { check } from "./check.js";
|
|
6
|
+
export { bundleForDeploy } from "./bundle.js";
|
|
7
|
+
export { link } from "./link.js";
|
|
8
|
+
export { clone } from "./clone.js";
|
|
9
|
+
export { deploy } from "./deploy.js";
|
|
10
|
+
export { run, parseJsonInput } from "./run.js";
|
|
11
|
+
export { SSE_EVENT_TYPES } from "./types.js";
|
|
12
|
+
export { decodeExecutionProjection } from "./decode.js";
|
|
13
|
+
export { inspect, listExecutions, inspectBuild, waitForExecution, isExecutionTerminal, } from "./inspect.js";
|
|
14
|
+
export { watchExecution, parseSseFrame, parseSseEvent } from "./watch.js";
|
|
15
|
+
export { signal, parseSignalPayload } from "./signal.js";
|
|
16
|
+
export { createSchedule, listSchedules, getSchedule, cancelSchedule, previewCron } from "./schedule.js";
|
|
17
|
+
export { assertDeployable, pushHead, cloneRepo, redactCredentials } from "./git.js";
|
|
18
|
+
export { parseStubFile, STUB_FILE_VERSION } from "./local/stubs.js";
|
|
19
|
+
export { runLocal, runLocalFromDir, STUBS_FILE } from "./local/run-local.js";
|
|
20
|
+
export { loadDefinition } from "./local/load.js";
|
|
21
|
+
export { LocalStubDispatcher } from "./local/dispatcher.js";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { GatewayClient } from "./client.js";
|
|
2
|
+
import type { ExecutionProjection, ExecutionRef, SseEvent } from "./types.js";
|
|
3
|
+
export interface BuildDetail {
|
|
4
|
+
id?: string;
|
|
5
|
+
status: string;
|
|
6
|
+
error?: unknown;
|
|
7
|
+
}
|
|
8
|
+
export interface InspectOptions {
|
|
9
|
+
executionId: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function inspect(opts: InspectOptions, client: GatewayClient): Promise<ExecutionProjection>;
|
|
12
|
+
export declare function isExecutionTerminal(status: string): boolean;
|
|
13
|
+
export type WaitStopReason = "terminal" | "needs-signal" | "timeout";
|
|
14
|
+
export interface WaitForExecutionOptions {
|
|
15
|
+
executionId: string;
|
|
16
|
+
maxWaitMs?: number;
|
|
17
|
+
pollMs?: number;
|
|
18
|
+
autoResumeSignals?: string[];
|
|
19
|
+
sleep?: (ms: number) => Promise<void>;
|
|
20
|
+
now?: () => number;
|
|
21
|
+
watch?: (opts: {
|
|
22
|
+
executionId: string;
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
}, client: GatewayClient) => AsyncIterable<SseEvent>;
|
|
25
|
+
}
|
|
26
|
+
export interface WaitForExecutionResult {
|
|
27
|
+
execution: ExecutionProjection;
|
|
28
|
+
reason: WaitStopReason;
|
|
29
|
+
done: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare function waitForExecution(opts: WaitForExecutionOptions, client: GatewayClient): Promise<WaitForExecutionResult>;
|
|
32
|
+
export declare function listExecutions(client: GatewayClient): Promise<ExecutionRef[]>;
|
|
33
|
+
export interface InspectBuildOptions {
|
|
34
|
+
definitionId: string;
|
|
35
|
+
buildRunId: string;
|
|
36
|
+
}
|
|
37
|
+
export interface InspectBuildResult {
|
|
38
|
+
build: BuildDetail;
|
|
39
|
+
}
|
|
40
|
+
export declare function inspectBuild(opts: InspectBuildOptions, client: GatewayClient): Promise<InspectBuildResult>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { decodeExecutionProjection, decodeExecutionRef } from "./decode.js";
|
|
2
|
+
import { watchExecution } from "./watch.js";
|
|
3
|
+
export async function inspect(opts, client) {
|
|
4
|
+
const raw = await client.get(`/executions/${opts.executionId}`);
|
|
5
|
+
return decodeExecutionProjection(raw);
|
|
6
|
+
}
|
|
7
|
+
const TERMINAL_STATUSES = new Set([
|
|
8
|
+
"completed",
|
|
9
|
+
"failed",
|
|
10
|
+
"cancelled",
|
|
11
|
+
"canceled",
|
|
12
|
+
]);
|
|
13
|
+
export function isExecutionTerminal(status) {
|
|
14
|
+
return TERMINAL_STATUSES.has(status);
|
|
15
|
+
}
|
|
16
|
+
const AUTO_RESUME_PAUSE_SIGNALS = [
|
|
17
|
+
"models.coding.result",
|
|
18
|
+
"contentGeneration.video.result",
|
|
19
|
+
];
|
|
20
|
+
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
function settledResult(execution, autoResume) {
|
|
22
|
+
if (isExecutionTerminal(execution.status)) {
|
|
23
|
+
return { reason: "terminal", done: true };
|
|
24
|
+
}
|
|
25
|
+
if (execution.status === "paused") {
|
|
26
|
+
const signal = execution.pausedSignalName ?? null;
|
|
27
|
+
if (!signal || !autoResume.includes(signal)) {
|
|
28
|
+
return { reason: "needs-signal", done: false };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
export async function waitForExecution(opts, client) {
|
|
34
|
+
const maxWaitMs = opts.maxWaitMs ?? 45000;
|
|
35
|
+
const autoResume = opts.autoResumeSignals ?? AUTO_RESUME_PAUSE_SIGNALS;
|
|
36
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
37
|
+
const now = opts.now ?? Date.now;
|
|
38
|
+
const watch = opts.watch ?? watchExecution;
|
|
39
|
+
const deadline = now() + maxWaitMs;
|
|
40
|
+
const read = () => inspect({ executionId: opts.executionId }, client);
|
|
41
|
+
let execution = await read();
|
|
42
|
+
let settled = settledResult(execution, autoResume);
|
|
43
|
+
if (settled)
|
|
44
|
+
return { execution, ...settled };
|
|
45
|
+
if (now() >= deadline)
|
|
46
|
+
return { execution, reason: "timeout", done: false };
|
|
47
|
+
const abort = new AbortController();
|
|
48
|
+
const timer = setTimeout(() => abort.abort(), Math.max(0, deadline - now()));
|
|
49
|
+
timer.unref?.();
|
|
50
|
+
const events = watch({ executionId: opts.executionId, signal: abort.signal }, client)[Symbol.asyncIterator]();
|
|
51
|
+
try {
|
|
52
|
+
for (;;) {
|
|
53
|
+
const next = await events.next();
|
|
54
|
+
if (next.done)
|
|
55
|
+
break;
|
|
56
|
+
execution = await read();
|
|
57
|
+
settled = settledResult(execution, autoResume);
|
|
58
|
+
if (settled)
|
|
59
|
+
return { execution, ...settled };
|
|
60
|
+
if (now() >= deadline)
|
|
61
|
+
return { execution, reason: "timeout", done: false };
|
|
62
|
+
}
|
|
63
|
+
if (now() >= deadline)
|
|
64
|
+
return { execution, reason: "timeout", done: false };
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
await events.return?.(undefined);
|
|
71
|
+
}
|
|
72
|
+
let interval = opts.pollMs ?? 1000;
|
|
73
|
+
for (;;) {
|
|
74
|
+
const remaining = deadline - now();
|
|
75
|
+
if (remaining <= 0)
|
|
76
|
+
return { execution, reason: "timeout", done: false };
|
|
77
|
+
await sleep(Math.min(interval, remaining));
|
|
78
|
+
interval = Math.min(interval * 1.5, 5000);
|
|
79
|
+
execution = await read();
|
|
80
|
+
settled = settledResult(execution, autoResume);
|
|
81
|
+
if (settled)
|
|
82
|
+
return { execution, ...settled };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export async function listExecutions(client) {
|
|
86
|
+
const raw = await client.get("/executions");
|
|
87
|
+
return Array.isArray(raw) ? raw.map(decodeExecutionRef) : [];
|
|
88
|
+
}
|
|
89
|
+
export async function inspectBuild(opts, client) {
|
|
90
|
+
const build = await client.get(`/definitions/${opts.definitionId}/builds/${opts.buildRunId}`);
|
|
91
|
+
return { build };
|
|
92
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { GatewayClient } from './client.js';
|
|
2
|
+
export interface DefinitionSummary {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
slug?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface LinkOptions {
|
|
8
|
+
name: string;
|
|
9
|
+
create?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface LinkResult {
|
|
12
|
+
definitionId: string;
|
|
13
|
+
name: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function link(opts: LinkOptions, client: GatewayClient): Promise<LinkResult>;
|
package/dist/esm/link.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AgentOperationError } from './errors.js';
|
|
2
|
+
export async function link(opts, client) {
|
|
3
|
+
const list = await client.get('/definitions');
|
|
4
|
+
let def = list.find((d) => d.name === opts.name || d.slug === opts.name);
|
|
5
|
+
if (!def) {
|
|
6
|
+
if (!opts.create) {
|
|
7
|
+
throw new AgentOperationError({
|
|
8
|
+
code: 'NOT_FOUND',
|
|
9
|
+
message: `No agent named '${opts.name}'.`,
|
|
10
|
+
hint: 'Create it with { create: true }, or pass the name of an existing one.',
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
def = await client.post('/definitions', { name: opts.name });
|
|
14
|
+
}
|
|
15
|
+
return { definitionId: def.id, name: def.name };
|
|
16
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type NextStepDirective, type AgentDefinition } from "@sapiom/agent";
|
|
2
|
+
import { type StepDispatcher, type StepDispatchRequest, type AgentRunnerCore } from "@sapiom/agent-runtime";
|
|
3
|
+
import type { StubFile } from "./stubs.js";
|
|
4
|
+
export interface LogEntry {
|
|
5
|
+
level: "info" | "warn" | "error" | "debug";
|
|
6
|
+
msg: string;
|
|
7
|
+
meta?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
export interface LocalStepTrace {
|
|
10
|
+
step: string;
|
|
11
|
+
attempt: number;
|
|
12
|
+
input: unknown;
|
|
13
|
+
status: "succeeded" | "threw";
|
|
14
|
+
output?: unknown;
|
|
15
|
+
directive?: NextStepDirective;
|
|
16
|
+
error?: {
|
|
17
|
+
name: string;
|
|
18
|
+
message: string;
|
|
19
|
+
stack?: string;
|
|
20
|
+
};
|
|
21
|
+
logs: LogEntry[];
|
|
22
|
+
}
|
|
23
|
+
export declare class LocalStubDispatcher implements StepDispatcher {
|
|
24
|
+
private readonly definition;
|
|
25
|
+
private readonly stubs;
|
|
26
|
+
private core;
|
|
27
|
+
private maxAttemptsPerStep;
|
|
28
|
+
private signals;
|
|
29
|
+
readonly trace: LocalStepTrace[];
|
|
30
|
+
readonly usedKeysByStep: Map<string, Set<string>>;
|
|
31
|
+
readonly stubWarnings: Set<string>;
|
|
32
|
+
constructor(definition: AgentDefinition, stubs: StubFile);
|
|
33
|
+
setCore(core: AgentRunnerCore): void;
|
|
34
|
+
setMaxAttempts(max: number): void;
|
|
35
|
+
setSignals(signals: Map<string, unknown>): void;
|
|
36
|
+
dispatch(request: StepDispatchRequest): Promise<void>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { InMemoryContextStore, } from "@sapiom/agent";
|
|
2
|
+
import { parseCorrelationId, STEP_COMPLETION_OUTCOME, } from "@sapiom/agent-runtime";
|
|
3
|
+
import { createStubClient } from "@sapiom/tools/stub";
|
|
4
|
+
export class LocalStubDispatcher {
|
|
5
|
+
constructor(definition, stubs) {
|
|
6
|
+
this.definition = definition;
|
|
7
|
+
this.stubs = stubs;
|
|
8
|
+
this.core = null;
|
|
9
|
+
this.signals = new Map();
|
|
10
|
+
this.trace = [];
|
|
11
|
+
this.usedKeysByStep = new Map();
|
|
12
|
+
this.stubWarnings = new Set();
|
|
13
|
+
}
|
|
14
|
+
setCore(core) {
|
|
15
|
+
this.core = core;
|
|
16
|
+
}
|
|
17
|
+
setMaxAttempts(max) {
|
|
18
|
+
this.maxAttemptsPerStep = max;
|
|
19
|
+
}
|
|
20
|
+
setSignals(signals) {
|
|
21
|
+
this.signals = signals;
|
|
22
|
+
}
|
|
23
|
+
async dispatch(request) {
|
|
24
|
+
if (!this.core)
|
|
25
|
+
throw new Error("LocalStubDispatcher: setCore() was not called");
|
|
26
|
+
const step = this.definition.steps[request.stepName];
|
|
27
|
+
if (!step)
|
|
28
|
+
throw new Error(`LocalStubDispatcher: no step '${request.stepName}' in the definition`);
|
|
29
|
+
const parsed = parseCorrelationId(request.correlationId);
|
|
30
|
+
if (!parsed)
|
|
31
|
+
throw new Error(`LocalStubDispatcher: malformed correlationId '${request.correlationId}'`);
|
|
32
|
+
const logs = [];
|
|
33
|
+
const sharedStore = new InMemoryContextStore(request.shared);
|
|
34
|
+
const overrides = (this.stubs.steps[request.stepName] ?? {});
|
|
35
|
+
let usedKeys = this.usedKeysByStep.get(request.stepName);
|
|
36
|
+
if (!usedKeys) {
|
|
37
|
+
usedKeys = new Set();
|
|
38
|
+
this.usedKeysByStep.set(request.stepName, usedKeys);
|
|
39
|
+
}
|
|
40
|
+
const sapiom = createStubClient({
|
|
41
|
+
overrides,
|
|
42
|
+
signals: this.signals,
|
|
43
|
+
usedKeys,
|
|
44
|
+
warnings: this.stubWarnings,
|
|
45
|
+
});
|
|
46
|
+
const ctx = {
|
|
47
|
+
executionId: request.executionId,
|
|
48
|
+
workflowName: request.workflowName,
|
|
49
|
+
organizationId: request.organizationId,
|
|
50
|
+
tenantId: request.tenantId,
|
|
51
|
+
input: request.workflowInput,
|
|
52
|
+
shared: sharedStore,
|
|
53
|
+
history: [],
|
|
54
|
+
attempts: request.attempt,
|
|
55
|
+
logger: makeLogger(logs),
|
|
56
|
+
sapiom,
|
|
57
|
+
};
|
|
58
|
+
let directive;
|
|
59
|
+
try {
|
|
60
|
+
directive = await step.run(request.input, ctx);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
64
|
+
this.trace.push({
|
|
65
|
+
step: request.stepName,
|
|
66
|
+
attempt: request.attempt,
|
|
67
|
+
input: request.input,
|
|
68
|
+
status: "threw",
|
|
69
|
+
error: { name: e.name, message: e.message, stack: e.stack },
|
|
70
|
+
logs,
|
|
71
|
+
});
|
|
72
|
+
await this.core.completeDispatchedStep({
|
|
73
|
+
protocol: 1,
|
|
74
|
+
correlationId: request.correlationId,
|
|
75
|
+
outcome: STEP_COMPLETION_OUTCOME.THREW,
|
|
76
|
+
error: { name: e.name, message: e.message, stack: e.stack },
|
|
77
|
+
shared: sharedStore.snapshot(),
|
|
78
|
+
}, parsed, this.maxAttemptsPerStep);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const { output, wire } = splitDirective(directive);
|
|
82
|
+
this.trace.push({
|
|
83
|
+
step: request.stepName,
|
|
84
|
+
attempt: request.attempt,
|
|
85
|
+
input: request.input,
|
|
86
|
+
status: "succeeded",
|
|
87
|
+
output,
|
|
88
|
+
directive,
|
|
89
|
+
logs,
|
|
90
|
+
});
|
|
91
|
+
const payload = {
|
|
92
|
+
protocol: 1,
|
|
93
|
+
correlationId: request.correlationId,
|
|
94
|
+
outcome: STEP_COMPLETION_OUTCOME.RESULT,
|
|
95
|
+
result: { output, directive: wire },
|
|
96
|
+
shared: sharedStore.snapshot(),
|
|
97
|
+
};
|
|
98
|
+
await this.core.completeDispatchedStep(payload, parsed, this.maxAttemptsPerStep);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function makeLogger(sink) {
|
|
102
|
+
const at = (level) => (msg, meta) => {
|
|
103
|
+
sink.push({ level, msg, ...(meta ? { meta } : {}) });
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
info: at("info"),
|
|
107
|
+
warn: at("warn"),
|
|
108
|
+
error: at("error"),
|
|
109
|
+
debug: at("debug"),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function splitDirective(d) {
|
|
113
|
+
switch (d.kind) {
|
|
114
|
+
case "continue":
|
|
115
|
+
return {
|
|
116
|
+
output: d.input,
|
|
117
|
+
wire: { kind: "continue", stepName: d.stepName, input: d.input },
|
|
118
|
+
};
|
|
119
|
+
case "terminate":
|
|
120
|
+
return {
|
|
121
|
+
output: d.output,
|
|
122
|
+
wire: { kind: "terminate", reason: d.reason },
|
|
123
|
+
};
|
|
124
|
+
case "fail":
|
|
125
|
+
return {
|
|
126
|
+
output: d.output,
|
|
127
|
+
wire: { kind: "fail", reason: d.reason },
|
|
128
|
+
};
|
|
129
|
+
case "pause_until_signal":
|
|
130
|
+
return {
|
|
131
|
+
output: d.output,
|
|
132
|
+
wire: {
|
|
133
|
+
kind: "pause_until_signal",
|
|
134
|
+
signal: d.signal,
|
|
135
|
+
timeoutMs: d.timeoutMs,
|
|
136
|
+
resumeStep: d.resumeStep,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
case "retry":
|
|
140
|
+
return {
|
|
141
|
+
output: undefined,
|
|
142
|
+
wire: { kind: "retry", delayMs: d.delayMs, reason: d.reason },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { buildManifest, isAgentDefinition, agentManifestSchema, } from '@sapiom/agent';
|
|
6
|
+
import * as esbuild from 'esbuild';
|
|
7
|
+
import { AgentOperationError } from '../errors.js';
|
|
8
|
+
const LOCAL_SDK_VERSION = '0.0.0-local';
|
|
9
|
+
export async function loadDefinition(sourceDir) {
|
|
10
|
+
const entryFile = path.join(sourceDir, 'index.ts');
|
|
11
|
+
if (!existsSync(entryFile)) {
|
|
12
|
+
throw new AgentOperationError({
|
|
13
|
+
code: 'NO_ENTRY',
|
|
14
|
+
message: `No index.ts found in ${sourceDir}.`,
|
|
15
|
+
hint: 'Run this from an agent project, or pass its directory.',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const tmp = mkdtempSync(path.join(tmpdir(), 'sapiom-run-'));
|
|
19
|
+
const bundlePath = path.join(tmp, 'definition.mjs');
|
|
20
|
+
try {
|
|
21
|
+
try {
|
|
22
|
+
await esbuild.build({
|
|
23
|
+
entryPoints: [entryFile],
|
|
24
|
+
outfile: bundlePath,
|
|
25
|
+
bundle: true,
|
|
26
|
+
platform: 'node',
|
|
27
|
+
target: 'node20',
|
|
28
|
+
format: 'esm',
|
|
29
|
+
logLevel: 'silent',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
throw new AgentOperationError({
|
|
34
|
+
code: 'BUNDLE_FAILED',
|
|
35
|
+
message: 'Failed to bundle the agent.',
|
|
36
|
+
hint: err instanceof Error ? err.message : String(err),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const mod = await import(`file://${bundlePath}?t=${Date.now()}`);
|
|
40
|
+
const defs = Object.values(mod).filter(isAgentDefinition);
|
|
41
|
+
if (defs.length === 0) {
|
|
42
|
+
throw new AgentOperationError({
|
|
43
|
+
code: 'NO_DEFINITION',
|
|
44
|
+
message: 'No agent was exported from index.ts.',
|
|
45
|
+
hint: 'Export the result of defineAgent({ … }).',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (defs.length > 1) {
|
|
49
|
+
throw new AgentOperationError({
|
|
50
|
+
code: 'MULTIPLE_DEFINITIONS',
|
|
51
|
+
message: 'index.ts exports more than one agent.',
|
|
52
|
+
hint: 'Export exactly one defineAgent({ … }) result.',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const definition = defs[0];
|
|
56
|
+
const sha256 = createHash('sha256').update(readFileSync(bundlePath)).digest('hex');
|
|
57
|
+
const manifest = agentManifestSchema.parse(buildManifest(definition, { sdkVersion: LOCAL_SDK_VERSION, artifact: { sha256, entryFile: 'definition.mjs' } }));
|
|
58
|
+
return { definition, manifest };
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AgentDefinition, AgentManifest } from "@sapiom/agent";
|
|
2
|
+
import { type LocalStepTrace } from "./dispatcher.js";
|
|
3
|
+
import { type StubFile } from "./stubs.js";
|
|
4
|
+
export declare const STUBS_FILE: string;
|
|
5
|
+
export interface RunLocalOptions {
|
|
6
|
+
definition: AgentDefinition;
|
|
7
|
+
manifest: AgentManifest;
|
|
8
|
+
input?: unknown;
|
|
9
|
+
stubs?: StubFile;
|
|
10
|
+
maxAttemptsPerStep?: number;
|
|
11
|
+
}
|
|
12
|
+
export type LocalRunOutcome = "completed" | "failed" | "paused" | "running";
|
|
13
|
+
export interface UnusedStub {
|
|
14
|
+
step: string;
|
|
15
|
+
key: string;
|
|
16
|
+
}
|
|
17
|
+
export interface LocalRunResult {
|
|
18
|
+
outcome: LocalRunOutcome;
|
|
19
|
+
executionId: string;
|
|
20
|
+
output?: unknown;
|
|
21
|
+
error?: unknown;
|
|
22
|
+
steps: LocalStepTrace[];
|
|
23
|
+
unusedStubs: UnusedStub[];
|
|
24
|
+
stubWarnings: string[];
|
|
25
|
+
}
|
|
26
|
+
export declare function runLocal(opts: RunLocalOptions): Promise<LocalRunResult>;
|
|
27
|
+
export declare function runLocalFromDir(opts: {
|
|
28
|
+
sourceDir: string;
|
|
29
|
+
input?: unknown;
|
|
30
|
+
stubs?: StubFile;
|
|
31
|
+
maxAttemptsPerStep?: number;
|
|
32
|
+
}): Promise<LocalRunResult>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { DEFAULT_MAX_ATTEMPTS_PER_STEP, InMemoryExecutionStore, NOOP_OBSERVER, AgentRunnerCore, } from "@sapiom/agent-runtime";
|
|
4
|
+
import { AgentOperationError } from "../errors.js";
|
|
5
|
+
import { LocalStubDispatcher } from "./dispatcher.js";
|
|
6
|
+
import { loadDefinition } from "./load.js";
|
|
7
|
+
import { parseStubFile } from "./stubs.js";
|
|
8
|
+
export const STUBS_FILE = path.join(".sapiom-dev", "stubs.json");
|
|
9
|
+
function loadStubsFile(sourceDir) {
|
|
10
|
+
const file = path.join(sourceDir, STUBS_FILE);
|
|
11
|
+
if (!existsSync(file))
|
|
12
|
+
return undefined;
|
|
13
|
+
let raw;
|
|
14
|
+
try {
|
|
15
|
+
raw = JSON.parse(readFileSync(file, "utf8"));
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
throw new AgentOperationError({
|
|
19
|
+
code: "STUBS_INVALID",
|
|
20
|
+
message: `${STUBS_FILE} is not valid JSON.`,
|
|
21
|
+
hint: err instanceof Error ? err.message : String(err),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return parseStubFile(raw);
|
|
25
|
+
}
|
|
26
|
+
const MAX_ADVANCES = 1000;
|
|
27
|
+
export async function runLocal(opts) {
|
|
28
|
+
const stubs = opts.stubs ?? { version: 1, steps: {} };
|
|
29
|
+
const max = opts.maxAttemptsPerStep ?? DEFAULT_MAX_ATTEMPTS_PER_STEP;
|
|
30
|
+
const store = new InMemoryExecutionStore();
|
|
31
|
+
const dispatcher = new LocalStubDispatcher(opts.definition, stubs);
|
|
32
|
+
const signals = new Map();
|
|
33
|
+
dispatcher.setSignals(signals);
|
|
34
|
+
const core = new AgentRunnerCore({
|
|
35
|
+
store,
|
|
36
|
+
dispatcher,
|
|
37
|
+
observer: NOOP_OBSERVER,
|
|
38
|
+
});
|
|
39
|
+
dispatcher.setCore(core);
|
|
40
|
+
dispatcher.setMaxAttempts(max);
|
|
41
|
+
const executionId = await core.createExecution(opts.definition.name, opts.definition.entry, opts.input, {
|
|
42
|
+
manifest: opts.manifest,
|
|
43
|
+
});
|
|
44
|
+
let guard = 0;
|
|
45
|
+
while (guard++ < MAX_ADVANCES) {
|
|
46
|
+
await core.advance(executionId, max);
|
|
47
|
+
const row = await store.loadExecution(executionId);
|
|
48
|
+
if (!row ||
|
|
49
|
+
row.status === "completed" ||
|
|
50
|
+
row.status === "failed" ||
|
|
51
|
+
row.status === "cancelled")
|
|
52
|
+
break;
|
|
53
|
+
if (row.status === "paused") {
|
|
54
|
+
const payload = signals.get(row.pausedSignalCorrelationId ?? "") ?? {};
|
|
55
|
+
await core.resetForResume(executionId, { fromStepInput: payload });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const final = await store.loadExecution(executionId);
|
|
59
|
+
const outcome = final?.status === "cancelled" ? "failed" : (final?.status ?? "running");
|
|
60
|
+
const unusedStubs = [];
|
|
61
|
+
for (const [step, used] of dispatcher.usedKeysByStep) {
|
|
62
|
+
for (const key of Object.keys(stubs.steps[step] ?? {})) {
|
|
63
|
+
if (!used.has(key))
|
|
64
|
+
unusedStubs.push({ step, key });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
outcome,
|
|
69
|
+
executionId,
|
|
70
|
+
output: final?.output,
|
|
71
|
+
error: final?.error,
|
|
72
|
+
steps: dispatcher.trace,
|
|
73
|
+
unusedStubs,
|
|
74
|
+
stubWarnings: [...dispatcher.stubWarnings],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export async function runLocalFromDir(opts) {
|
|
78
|
+
const { definition, manifest } = await loadDefinition(opts.sourceDir);
|
|
79
|
+
const stubs = opts.stubs ?? loadStubsFile(opts.sourceDir);
|
|
80
|
+
return runLocal({
|
|
81
|
+
definition,
|
|
82
|
+
manifest,
|
|
83
|
+
input: opts.input,
|
|
84
|
+
stubs,
|
|
85
|
+
maxAttemptsPerStep: opts.maxAttemptsPerStep,
|
|
86
|
+
});
|
|
87
|
+
}
|