@sapiom/agent-core
Advanced tools
+12
-0
| # @sapiom/orchestration-core | ||
| ## 0.10.3 | ||
| ### Patch Changes | ||
| - 1000510: Describe deploy as a synthesized bundle of current local source, distinguish | ||
| account-free local validation from metered cloud builds and production runs, | ||
| and make execution inspection's cost-agnostic evidence boundary explicit. | ||
| - 25fc26f: Make local agent runs parse step inputs through their Zod schemas like production, normalize relative check directories, stage and retry gallery clones across branch-propagation delays, and clarify that local validation stubs Sapiom capability traffic without sandboxing arbitrary author-code side effects. | ||
| - 9addb66: Keep Agent Studio workspace discovery consistent across scanning, live updates, and the folder picker; preserve Studio state when cloning gallery templates; and clarify bundled starter network requirements. | ||
| - Updated dependencies [1ac32ef] | ||
| - @sapiom/tools@0.26.1 | ||
| ## 0.10.2 | ||
@@ -4,0 +16,0 @@ |
@@ -0,1 +1,2 @@ | ||
| export declare function runTypecheck(sourceDir: string): string | null; | ||
| export interface CheckOptions { | ||
@@ -2,0 +3,0 @@ sourceDir: string; |
+48
-35
@@ -39,2 +39,3 @@ "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.runTypecheck = runTypecheck; | ||
| exports.entryInputSchemaWarning = entryInputSchemaWarning; | ||
@@ -51,8 +52,11 @@ exports.check = check; | ||
| function runTypecheck(sourceDir) { | ||
| const tscBin = node_path_1.default.join(sourceDir, 'node_modules', '.bin', 'tsc'); | ||
| const tscBin = node_path_1.default.join(sourceDir, "node_modules", ".bin", "tsc"); | ||
| if (!(0, node_fs_1.existsSync)(tscBin)) { | ||
| return 'typecheck skipped — TypeScript is not installed (run npm install first)'; | ||
| return "typecheck skipped — TypeScript is not installed (run npm install first)"; | ||
| } | ||
| try { | ||
| (0, node_child_process_1.execFileSync)(tscBin, ['--noEmit'], { cwd: sourceDir, stdio: ['ignore', 'pipe', 'pipe'] }); | ||
| (0, node_child_process_1.execFileSync)(tscBin, ["--noEmit"], { | ||
| cwd: sourceDir, | ||
| stdio: ["ignore", "pipe", "pipe"], | ||
| }); | ||
| return null; | ||
@@ -62,11 +66,12 @@ } | ||
| const e = err; | ||
| const output = (e.stdout?.toString() ?? '').trim() || (e.stderr?.toString() ?? '').trim(); | ||
| const output = (e.stdout?.toString() ?? "").trim() || | ||
| (e.stderr?.toString() ?? "").trim(); | ||
| throw new errors_js_1.AgentOperationError({ | ||
| code: 'TYPECHECK_FAILED', | ||
| message: 'The agent has type errors.', | ||
| hint: output || 'Run `tsc --noEmit` for details.', | ||
| code: "TYPECHECK_FAILED", | ||
| message: "The agent has type errors.", | ||
| hint: output || "Run `tsc --noEmit` for details.", | ||
| }); | ||
| } | ||
| } | ||
| const LOCAL_SDK_VERSION = '0.0.0-local'; | ||
| const LOCAL_SDK_VERSION = "0.0.0-local"; | ||
| function entryInputSchemaWarning(manifest) { | ||
@@ -80,9 +85,9 @@ const entryStep = manifest.steps[manifest.entry]; | ||
| async function check(opts) { | ||
| const { sourceDir } = opts; | ||
| const entryFile = node_path_1.default.join(sourceDir, 'index.ts'); | ||
| const sourceDir = node_path_1.default.resolve(opts.sourceDir); | ||
| const entryFile = node_path_1.default.join(sourceDir, "index.ts"); | ||
| if (!(0, node_fs_1.existsSync)(entryFile)) { | ||
| throw new errors_js_1.AgentOperationError({ | ||
| code: 'NO_ENTRY', | ||
| code: "NO_ENTRY", | ||
| message: `No index.ts found in ${sourceDir}.`, | ||
| hint: 'Run this from an agent project, or pass its directory.', | ||
| hint: "Run this from an agent project, or pass its directory.", | ||
| }); | ||
@@ -96,4 +101,4 @@ } | ||
| } | ||
| const tmp = (0, node_fs_1.mkdtempSync)(node_path_1.default.join((0, node_os_1.tmpdir)(), 'sapiom-check-')); | ||
| const bundlePath = node_path_1.default.join(tmp, 'definition.mjs'); | ||
| const tmp = (0, node_fs_1.mkdtempSync)(node_path_1.default.join((0, node_os_1.tmpdir)(), "sapiom-check-")); | ||
| const bundlePath = node_path_1.default.join(tmp, "definition.mjs"); | ||
| try { | ||
@@ -105,6 +110,6 @@ try { | ||
| bundle: true, | ||
| platform: 'node', | ||
| target: 'node20', | ||
| format: 'esm', | ||
| logLevel: 'silent', | ||
| platform: "node", | ||
| target: "node20", | ||
| format: "esm", | ||
| logLevel: "silent", | ||
| }); | ||
@@ -114,4 +119,4 @@ } | ||
| throw new errors_js_1.AgentOperationError({ | ||
| code: 'BUNDLE_FAILED', | ||
| message: 'Failed to bundle the agent.', | ||
| code: "BUNDLE_FAILED", | ||
| message: "Failed to bundle the agent.", | ||
| hint: err instanceof Error ? err.message : String(err), | ||
@@ -123,3 +128,4 @@ }); | ||
| for (const value of Object.values(mod)) { | ||
| if (((0, agent_1.isAgentDefinition)(value) || (0, agent_1.isLegacyOrchestrationDefinition)(value)) && !defs.includes(value)) { | ||
| if (((0, agent_1.isAgentDefinition)(value) || (0, agent_1.isLegacyOrchestrationDefinition)(value)) && | ||
| !defs.includes(value)) { | ||
| defs.push(value); | ||
@@ -130,5 +136,5 @@ } | ||
| throw new errors_js_1.AgentOperationError({ | ||
| code: 'NO_DEFINITION', | ||
| message: 'No agent was exported from index.ts.', | ||
| hint: 'Export the result of defineAgent({ … }).', | ||
| code: "NO_DEFINITION", | ||
| message: "No agent was exported from index.ts.", | ||
| hint: "Export the result of defineAgent({ … }).", | ||
| }); | ||
@@ -138,17 +144,22 @@ } | ||
| throw new errors_js_1.AgentOperationError({ | ||
| code: 'MULTIPLE_DEFINITIONS', | ||
| message: 'index.ts exports more than one agent.', | ||
| hint: 'Export exactly one defineAgent({ … }) result.', | ||
| code: "MULTIPLE_DEFINITIONS", | ||
| message: "index.ts exports more than one agent.", | ||
| hint: "Export exactly one defineAgent({ … }) result.", | ||
| }); | ||
| } | ||
| const def = defs[0]; | ||
| const sha256 = (0, node_crypto_1.createHash)('sha256').update((0, node_fs_1.readFileSync)(bundlePath)).digest('hex'); | ||
| const sha256 = (0, node_crypto_1.createHash)("sha256") | ||
| .update((0, node_fs_1.readFileSync)(bundlePath)) | ||
| .digest("hex"); | ||
| let manifest; | ||
| try { | ||
| manifest = agent_1.agentManifestSchema.parse((0, agent_1.buildManifest)(def, { sdkVersion: LOCAL_SDK_VERSION, artifact: { sha256, entryFile: 'definition.mjs' } })); | ||
| manifest = agent_1.agentManifestSchema.parse((0, agent_1.buildManifest)(def, { | ||
| sdkVersion: LOCAL_SDK_VERSION, | ||
| artifact: { sha256, entryFile: "definition.mjs" }, | ||
| })); | ||
| } | ||
| catch (err) { | ||
| throw new errors_js_1.AgentOperationError({ | ||
| code: 'MANIFEST_INVALID', | ||
| message: 'The agent produced an invalid manifest.', | ||
| code: "MANIFEST_INVALID", | ||
| message: "The agent produced an invalid manifest.", | ||
| hint: err instanceof Error ? err.message : String(err), | ||
@@ -162,4 +173,4 @@ }); | ||
| throw new errors_js_1.AgentOperationError({ | ||
| code: 'GRAPH_INVALID', | ||
| message: 'The agent graph is invalid.', | ||
| code: "GRAPH_INVALID", | ||
| message: "The agent graph is invalid.", | ||
| hint: err instanceof Error ? err.message : String(err), | ||
@@ -172,4 +183,6 @@ }); | ||
| const steps = manifest.steps; | ||
| const stepCount = Array.isArray(steps) ? steps.length : Object.keys(steps ?? {}).length; | ||
| const name = manifest.name ?? 'agent'; | ||
| const stepCount = Array.isArray(steps) | ||
| ? steps.length | ||
| : Object.keys(steps ?? {}).length; | ||
| const name = manifest.name ?? "agent"; | ||
| return { name, stepCount, warnings, manifest }; | ||
@@ -176,0 +189,0 @@ } |
@@ -1,3 +0,3 @@ | ||
| import { GatewayClient } from './client.js'; | ||
| import { type CloneRepoOptions } from './git.js'; | ||
| import { GatewayClient } from "./client.js"; | ||
| import { type CloneRepoOptions } from "./git.js"; | ||
| export interface CloneOptions { | ||
@@ -4,0 +4,0 @@ templateId?: string; |
+83
-20
@@ -12,2 +12,13 @@ "use strict"; | ||
| const git_js_1 = require("./git.js"); | ||
| const CLONE_PROPAGATION_MAX_ATTEMPTS = 8; | ||
| const CLONE_PROPAGATION_DELAY_MS = 500; | ||
| const CLONE_PROPAGATION_ERROR = /remote branch .* not found|couldn't find remote ref|repository appears to be empty/i; | ||
| function isClonePropagationError(error) { | ||
| return (error instanceof errors_js_1.AgentOperationError && | ||
| error.code === "GIT_CLONE" && | ||
| CLONE_PROPAGATION_ERROR.test(error.hint ?? "")); | ||
| } | ||
| function delay(ms) { | ||
| return new Promise((resolve) => setTimeout(resolve, ms)); | ||
| } | ||
| async function clone(opts, client) { | ||
@@ -19,4 +30,4 @@ const { templateId, forkId, definitionId, targetDir } = opts; | ||
| throw new errors_js_1.AgentOperationError({ | ||
| code: 'BAD_INPUT', | ||
| message: 'Provide a templateId (to fork then clone), a forkId (to clone an existing fork), or a definitionId (to clone a deployed agent).', | ||
| code: "BAD_INPUT", | ||
| message: "Provide a templateId (to fork then clone), a forkId (to clone an existing fork), or a definitionId (to clone a deployed agent).", | ||
| }); | ||
@@ -26,12 +37,21 @@ } | ||
| throw new errors_js_1.AgentOperationError({ | ||
| code: 'BAD_INPUT', | ||
| message: 'Provide only one of templateId, forkId, or definitionId.', | ||
| hint: 'Use templateId to start from a gallery template, forkId to re-clone an existing fork, or definitionId to pull a deployed agent locally.', | ||
| code: "BAD_INPUT", | ||
| message: "Provide only one of templateId, forkId, or definitionId.", | ||
| hint: "Use templateId to start from a gallery template, forkId to re-clone an existing fork, or definitionId to pull a deployed agent locally.", | ||
| }); | ||
| } | ||
| if ((0, node_fs_1.existsSync)(targetDir) && (0, node_fs_1.readdirSync)(targetDir).length > 0) { | ||
| throw new errors_js_1.AgentOperationError({ | ||
| code: 'DIR_NOT_EMPTY', | ||
| message: `Target directory '${targetDir}' already exists and is not empty.`, | ||
| }); | ||
| let preserveStudioState = false; | ||
| if ((0, node_fs_1.existsSync)(targetDir)) { | ||
| const entries = (0, node_fs_1.readdirSync)(targetDir); | ||
| if (entries.length === 1 && entries[0] === ".sapiom") { | ||
| const studioState = (0, node_fs_1.lstatSync)(node_path_1.default.join(targetDir, ".sapiom")); | ||
| preserveStudioState = | ||
| studioState.isDirectory() && !studioState.isSymbolicLink(); | ||
| } | ||
| if (entries.length > 0 && !preserveStudioState) { | ||
| throw new errors_js_1.AgentOperationError({ | ||
| code: "DIR_NOT_EMPTY", | ||
| message: `Target directory '${targetDir}' already exists and is not empty.`, | ||
| }); | ||
| } | ||
| } | ||
@@ -48,6 +68,6 @@ let resolvedForkId = forkId; | ||
| : await client.post(`/forks/${encodeURIComponent(resolvedForkId)}/clone-token`, {}); | ||
| if (!token.cloneUrl.startsWith('https://')) { | ||
| if (!token.cloneUrl.startsWith("https://")) { | ||
| throw new errors_js_1.AgentOperationError({ | ||
| code: 'BAD_CLONE_URL', | ||
| message: 'The clone token endpoint returned an unexpected clone URL.', | ||
| code: "BAD_CLONE_URL", | ||
| message: "The clone token endpoint returned an unexpected clone URL.", | ||
| }); | ||
@@ -57,9 +77,52 @@ } | ||
| (0, node_fs_1.mkdirSync)(parent, { recursive: true }); | ||
| runClone({ | ||
| cloneUrl: token.cloneUrl, | ||
| targetDir, | ||
| branch: token.defaultBranch, | ||
| repoFullName: token.repoFullName, | ||
| cwd: parent, | ||
| }); | ||
| let stagedRoot = null; | ||
| let cloneTarget = null; | ||
| try { | ||
| for (let attempt = 1; attempt <= CLONE_PROPAGATION_MAX_ATTEMPTS; attempt++) { | ||
| stagedRoot = (0, node_fs_1.mkdtempSync)(node_path_1.default.join(parent, ".sapiom-clone-")); | ||
| cloneTarget = stagedRoot; | ||
| try { | ||
| runClone({ | ||
| cloneUrl: token.cloneUrl, | ||
| targetDir: cloneTarget, | ||
| branch: token.defaultBranch, | ||
| repoFullName: token.repoFullName, | ||
| cwd: parent, | ||
| }); | ||
| break; | ||
| } | ||
| catch (error) { | ||
| (0, node_fs_1.rmSync)(stagedRoot, { recursive: true, force: true }); | ||
| stagedRoot = null; | ||
| cloneTarget = null; | ||
| if (attempt === CLONE_PROPAGATION_MAX_ATTEMPTS || | ||
| !isClonePropagationError(error)) { | ||
| throw error; | ||
| } | ||
| await delay(CLONE_PROPAGATION_DELAY_MS); | ||
| } | ||
| } | ||
| if (!cloneTarget) { | ||
| throw new errors_js_1.AgentOperationError({ | ||
| code: "GIT_CLONE", | ||
| message: "git clone failed.", | ||
| }); | ||
| } | ||
| const clonedEntries = (0, node_fs_1.readdirSync)(cloneTarget); | ||
| if (clonedEntries.includes(".sapiom")) { | ||
| throw new errors_js_1.AgentOperationError({ | ||
| code: "STUDIO_STATE_CONFLICT", | ||
| message: "The cloned repository contains a reserved .sapiom directory.", | ||
| hint: "Remove .sapiom from the template repository, then try again.", | ||
| }); | ||
| } | ||
| (0, node_fs_1.mkdirSync)(targetDir, { recursive: true }); | ||
| for (const entry of clonedEntries) { | ||
| (0, node_fs_1.renameSync)(node_path_1.default.join(cloneTarget, entry), node_path_1.default.join(targetDir, entry)); | ||
| } | ||
| } | ||
| finally { | ||
| if (stagedRoot) | ||
| (0, node_fs_1.rmSync)(stagedRoot, { recursive: true, force: true }); | ||
| } | ||
| (0, config_js_1.writeConfig)(targetDir, { | ||
@@ -66,0 +129,0 @@ repoFullName: token.repoFullName, |
@@ -1,2 +0,2 @@ | ||
| import { GatewayClient } from './client.js'; | ||
| import { GatewayClient } from "./client.js"; | ||
| export interface DeployOptions { | ||
@@ -3,0 +3,0 @@ projectDir: string; |
+34
-29
@@ -15,11 +15,11 @@ "use strict"; | ||
| function isPushAuthError(err) { | ||
| if (!(err instanceof errors_js_1.AgentOperationError) || err.code !== 'GIT') | ||
| if (!(err instanceof errors_js_1.AgentOperationError) || err.code !== "GIT") | ||
| return false; | ||
| const text = ((err.hint ?? '') + ' ' + err.message).toLowerCase(); | ||
| return (text.includes('authentication failed') || | ||
| text.includes('could not read from') || | ||
| text.includes('the requested url returned error: 401') || | ||
| text.includes('the requested url returned error: 403')); | ||
| const text = ((err.hint ?? "") + " " + err.message).toLowerCase(); | ||
| return (text.includes("authentication failed") || | ||
| text.includes("could not read from") || | ||
| text.includes("the requested url returned error: 401") || | ||
| text.includes("the requested url returned error: 403")); | ||
| } | ||
| const TERMINAL = new Set(['ready', 'failed', 'cancelled', 'superseded']); | ||
| const TERMINAL = new Set(["ready", "failed", "cancelled", "superseded"]); | ||
| const POLL_DELAYS_MS = [1000, 2000, 3000, 5000, 5000, 8000, 10000]; | ||
@@ -32,8 +32,8 @@ const POLL_BUDGET_MS = 300000; | ||
| const result = await deployOperation(opts, client); | ||
| (0, analytics_js_1.getOrchestrationAnalytics)().track('workflow.deploy', { | ||
| (0, analytics_js_1.getOrchestrationAnalytics)().track("workflow.deploy", { | ||
| workflow_id: opts.definitionId, | ||
| branch: opts.branch ?? 'main', | ||
| branch: opts.branch ?? "main", | ||
| build_run_id: result.buildRunId, | ||
| build_status: result.status, | ||
| status: 'success', | ||
| status: "success", | ||
| duration_ms: Date.now() - startedAt, | ||
@@ -44,6 +44,6 @@ }); | ||
| catch (err) { | ||
| (0, analytics_js_1.getOrchestrationAnalytics)().track('workflow.deploy', { | ||
| (0, analytics_js_1.getOrchestrationAnalytics)().track("workflow.deploy", { | ||
| workflow_id: opts.definitionId, | ||
| branch: opts.branch ?? 'main', | ||
| status: 'error', | ||
| branch: opts.branch ?? "main", | ||
| status: "error", | ||
| error_code: (0, analytics_js_1.telemetryErrorCode)(err), | ||
@@ -56,10 +56,15 @@ duration_ms: Date.now() - startedAt, | ||
| async function deployOperation(opts, client) { | ||
| const { projectDir, definitionId, branch = 'main' } = opts; | ||
| const { projectDir, definitionId, branch = "main" } = opts; | ||
| (0, git_js_1.assertDeployable)(projectDir); | ||
| const { code, dependencies } = await (0, bundle_js_1.bundleForDeploy)(projectDir); | ||
| const { pushUrl } = await client.post(`/definitions/${definitionId}/push-credentials`, {}); | ||
| const treeDir = (0, node_fs_1.mkdtempSync)(node_path_1.default.join((0, node_os_1.tmpdir)(), 'sapiom-deploy-')); | ||
| const treeDir = (0, node_fs_1.mkdtempSync)(node_path_1.default.join((0, node_os_1.tmpdir)(), "sapiom-deploy-")); | ||
| try { | ||
| (0, node_fs_1.writeFileSync)(node_path_1.default.join(treeDir, 'index.ts'), code); | ||
| (0, node_fs_1.writeFileSync)(node_path_1.default.join(treeDir, 'package.json'), JSON.stringify({ name: 'agent-definition', private: true, type: 'module', dependencies }, null, 2) + '\n'); | ||
| (0, node_fs_1.writeFileSync)(node_path_1.default.join(treeDir, "index.ts"), code); | ||
| (0, node_fs_1.writeFileSync)(node_path_1.default.join(treeDir, "package.json"), JSON.stringify({ | ||
| name: "agent-definition", | ||
| private: true, | ||
| type: "module", | ||
| dependencies, | ||
| }, null, 2) + "\n"); | ||
| try { | ||
@@ -82,19 +87,19 @@ (0, git_js_1.pushSynthesizedTree)(treeDir, pushUrl, branch); | ||
| throw new errors_js_1.AgentOperationError({ | ||
| code: 'BUILD_NO_ID', | ||
| message: 'The build was triggered but no build id was returned.', | ||
| code: "BUILD_NO_ID", | ||
| message: "The build was triggered but no build id was returned.", | ||
| }); | ||
| } | ||
| const final = await pollBuild(client, definitionId, buildRunId); | ||
| if (final.status !== 'ready') { | ||
| if (final.status === 'superseded') { | ||
| if (final.status !== "ready") { | ||
| if (final.status === "superseded") { | ||
| throw new errors_js_1.AgentOperationError({ | ||
| code: 'BUILD_SUPERSEDED', | ||
| message: 'A newer deploy superseded this build.', | ||
| step: 'build', | ||
| code: "BUILD_SUPERSEDED", | ||
| message: "A newer deploy superseded this build.", | ||
| step: "build", | ||
| }); | ||
| } | ||
| throw new errors_js_1.AgentOperationError({ | ||
| code: 'BUILD_FAILED', | ||
| code: "BUILD_FAILED", | ||
| message: `Build ${final.status}.`, | ||
| step: 'build', | ||
| step: "build", | ||
| hint: final.error?.stack || final.error?.message, | ||
@@ -117,7 +122,7 @@ }); | ||
| throw new errors_js_1.AgentOperationError({ | ||
| code: 'BUILD_TIMEOUT', | ||
| message: 'Build did not finish in time.', | ||
| step: 'build', | ||
| code: "BUILD_TIMEOUT", | ||
| message: "Build did not finish in time.", | ||
| step: "build", | ||
| hint: `Check it later via the logs API for build ${buildRunId}`, | ||
| }); | ||
| } |
+44
-18
@@ -12,3 +12,7 @@ "use strict"; | ||
| try { | ||
| return (0, node_child_process_1.execFileSync)('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); | ||
| return (0, node_child_process_1.execFileSync)("git", args, { | ||
| cwd, | ||
| encoding: "utf8", | ||
| stdio: ["ignore", "pipe", "pipe"], | ||
| }).trim(); | ||
| } | ||
@@ -19,3 +23,3 @@ catch (err) { | ||
| throw new errors_js_1.AgentOperationError({ | ||
| code: 'GIT', | ||
| code: "GIT", | ||
| message: `git ${args[0]} failed.`, | ||
@@ -28,8 +32,11 @@ hint, | ||
| try { | ||
| (0, node_child_process_1.execFileSync)('git', ['rev-parse', '--is-inside-work-tree'], { cwd: dir, stdio: 'ignore' }); | ||
| (0, node_child_process_1.execFileSync)("git", ["rev-parse", "--is-inside-work-tree"], { | ||
| cwd: dir, | ||
| stdio: "ignore", | ||
| }); | ||
| } | ||
| catch { | ||
| throw new errors_js_1.AgentOperationError({ | ||
| code: 'NOT_GIT', | ||
| message: 'Not a git repository.', | ||
| code: "NOT_GIT", | ||
| message: "Not a git repository.", | ||
| hint: 'Initialize one: git init && git add -A && git commit -m "init"', | ||
@@ -39,8 +46,8 @@ }); | ||
| try { | ||
| (0, node_child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], { cwd: dir, stdio: 'ignore' }); | ||
| (0, node_child_process_1.execFileSync)("git", ["rev-parse", "HEAD"], { cwd: dir, stdio: "ignore" }); | ||
| } | ||
| catch { | ||
| throw new errors_js_1.AgentOperationError({ | ||
| code: 'NO_COMMITS', | ||
| message: 'This repository has no commits yet.', | ||
| code: "NO_COMMITS", | ||
| message: "This repository has no commits yet.", | ||
| hint: 'Commit your work: git add -A && git commit -m "…"', | ||
@@ -51,3 +58,3 @@ }); | ||
| function redactCredentials(text) { | ||
| return text.replace(/(https?:\/\/)[^@\s/]+@/gi, '$1***@'); | ||
| return text.replace(/(https?:\/\/)[^@\s/]+@/gi, "$1***@"); | ||
| } | ||
@@ -57,3 +64,13 @@ function cloneRepo(opts) { | ||
| try { | ||
| (0, node_child_process_1.execFileSync)('git', ['clone', '--depth', '1', '--single-branch', '--branch', branch, '--', cloneUrl, targetDir], { cwd, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' }); | ||
| (0, node_child_process_1.execFileSync)("git", [ | ||
| "clone", | ||
| "--depth", | ||
| "1", | ||
| "--single-branch", | ||
| "--branch", | ||
| branch, | ||
| "--", | ||
| cloneUrl, | ||
| targetDir, | ||
| ], { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" }); | ||
| } | ||
@@ -64,4 +81,4 @@ catch (err) { | ||
| throw new errors_js_1.AgentOperationError({ | ||
| code: 'GIT_CLONE', | ||
| message: 'git clone failed.', | ||
| code: "GIT_CLONE", | ||
| message: "git clone failed.", | ||
| hint: redactCredentials(raw), | ||
@@ -71,3 +88,3 @@ }); | ||
| try { | ||
| git(['remote', 'set-url', 'origin', `https://github.com/${repoFullName}.git`], targetDir); | ||
| git(["remote", "set-url", "origin", `https://github.com/${repoFullName}.git`], targetDir); | ||
| } | ||
@@ -78,9 +95,18 @@ catch { | ||
| function pushHead(dir, pushUrl, branch) { | ||
| git(['push', '--force', pushUrl, `HEAD:${branch}`], dir); | ||
| git(["push", "--force", pushUrl, `HEAD:${branch}`], dir); | ||
| } | ||
| function pushSynthesizedTree(treeDir, pushUrl, branch) { | ||
| git(['init', '-q', '-b', branch], treeDir); | ||
| git(['add', '-A'], treeDir); | ||
| git(['-c', 'user.email=deploy@sapiom.ai', '-c', 'user.name=Sapiom Deploy', 'commit', '-q', '-m', 'deploy'], treeDir); | ||
| git(['push', '--force', pushUrl, `HEAD:${branch}`], treeDir); | ||
| git(["init", "-q", "-b", branch], treeDir); | ||
| git(["add", "-A"], treeDir); | ||
| git([ | ||
| "-c", | ||
| "user.email=deploy@sapiom.ai", | ||
| "-c", | ||
| "user.name=Sapiom Deploy", | ||
| "commit", | ||
| "-q", | ||
| "-m", | ||
| "deploy", | ||
| ], treeDir); | ||
| git(["push", "--force", pushUrl, `HEAD:${branch}`], treeDir); | ||
| } |
@@ -32,5 +32,5 @@ export { AgentOperationError } from "./errors.js"; | ||
| export type { SendFeedbackOptions, SendFeedbackResult } from "./feedback.js"; | ||
| export { createSchedule, listSchedules, getSchedule, cancelSchedule, previewCron } from "./schedule.js"; | ||
| export { createSchedule, listSchedules, getSchedule, cancelSchedule, previewCron, } from "./schedule.js"; | ||
| export type { ScheduleKind, ScheduleStatus, SchedulePolicy, CreateScheduleOptions, ListSchedulesOptions, CronPreviewOptions, CronPreview, ScheduleSummary, ScheduleDetail, ScheduleFireRecord, } from "./schedule.js"; | ||
| export { assertDeployable, pushHead, cloneRepo, redactCredentials } from "./git.js"; | ||
| export { assertDeployable, pushHead, cloneRepo, redactCredentials, } from "./git.js"; | ||
| export type { CloneRepoOptions } from "./git.js"; | ||
@@ -37,0 +37,0 @@ export { parseStubFile, STUB_FILE_VERSION } from "./local/stubs.js"; |
@@ -63,5 +63,13 @@ "use strict"; | ||
| }; | ||
| let stepInput = request.input; | ||
| let directive; | ||
| try { | ||
| directive = await step.run(request.input, ctx); | ||
| if (step.inputSchema) { | ||
| const parsedInput = step.inputSchema.safeParse(request.input); | ||
| if (!parsedInput.success) { | ||
| throw new agent_1.StepInputValidationError(request.stepName, parsedInput.error.issues); | ||
| } | ||
| stepInput = parsedInput.data; | ||
| } | ||
| directive = await step.run(stepInput, ctx); | ||
| } | ||
@@ -73,3 +81,3 @@ catch (err) { | ||
| attempt: request.attempt, | ||
| input: request.input, | ||
| input: e instanceof agent_1.StepInputValidationError ? request.input : stepInput, | ||
| status: "threw", | ||
@@ -95,3 +103,3 @@ error: { name: e.name, message: e.message, stack: e.stack }, | ||
| attempt: request.attempt, | ||
| input: request.input, | ||
| input: stepInput, | ||
| status: "succeeded", | ||
@@ -98,0 +106,0 @@ output, |
| export declare const STUB_FILE_VERSION = 1; | ||
| export type StubResponse = unknown; | ||
| export type StepStubs = Record<string, StubResponse | StubResponse[]>; | ||
| export type StepStubs = Record<string, StubResponse>; | ||
| export interface StubFile { | ||
@@ -5,0 +5,0 @@ version: number; |
@@ -8,17 +8,21 @@ "use strict"; | ||
| function parseStubFile(raw) { | ||
| if (raw == null || typeof raw !== 'object' || Array.isArray(raw)) { | ||
| throw invalid('the stub file must be a JSON object.'); | ||
| if (raw == null || typeof raw !== "object" || Array.isArray(raw)) { | ||
| throw invalid("the stub file must be a JSON object."); | ||
| } | ||
| const obj = raw; | ||
| const version = obj.version ?? exports.STUB_FILE_VERSION; | ||
| if (typeof version !== 'number') { | ||
| throw invalid('`version` must be a number.'); | ||
| if (typeof version !== "number") { | ||
| throw invalid("`version` must be a number."); | ||
| } | ||
| const stepsRaw = obj.steps ?? {}; | ||
| if (stepsRaw == null || typeof stepsRaw !== 'object' || Array.isArray(stepsRaw)) { | ||
| throw invalid('`steps` must be an object keyed by step name.'); | ||
| if (stepsRaw == null || | ||
| typeof stepsRaw !== "object" || | ||
| Array.isArray(stepsRaw)) { | ||
| throw invalid("`steps` must be an object keyed by step name."); | ||
| } | ||
| const steps = {}; | ||
| for (const [stepName, stepStubs] of Object.entries(stepsRaw)) { | ||
| if (stepStubs == null || typeof stepStubs !== 'object' || Array.isArray(stepStubs)) { | ||
| if (stepStubs == null || | ||
| typeof stepStubs !== "object" || | ||
| Array.isArray(stepStubs)) { | ||
| throw invalid(`steps.${stepName} must be an object keyed by capability path.`); | ||
@@ -32,6 +36,6 @@ } | ||
| return new errors_js_1.AgentOperationError({ | ||
| code: 'STUBS_INVALID', | ||
| code: "STUBS_INVALID", | ||
| message: `Invalid stub file: ${detail}`, | ||
| hint: 'Expected { "version": 1, "steps": { "<step>": { "<capability.path>": <response> | [<response>] } } }.', | ||
| hint: 'Expected { "version": 1, "steps": { "<step>": { "<capability.path>": <response> } } }.', | ||
| }); | ||
| } |
| export declare const VERSION_FALLBACK: { | ||
| readonly agent: "0.9.3"; | ||
| readonly tools: "0.26.0"; | ||
| readonly tools: "0.26.1"; | ||
| }; |
@@ -6,3 +6,3 @@ "use strict"; | ||
| agent: "0.9.3", | ||
| tools: "0.26.0", | ||
| tools: "0.26.1", | ||
| }; |
@@ -0,1 +1,2 @@ | ||
| export declare function runTypecheck(sourceDir: string): string | null; | ||
| export interface CheckOptions { | ||
@@ -2,0 +3,0 @@ sourceDir: string; |
+56
-44
@@ -1,16 +0,19 @@ | ||
| import { execFileSync } from 'node:child_process'; | ||
| import { createHash } from 'node:crypto'; | ||
| import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'; | ||
| import { tmpdir } from 'node:os'; | ||
| import path from 'node:path'; | ||
| import { assertValidGraph, buildManifest, isAgentDefinition, isLegacyOrchestrationDefinition, agentManifestSchema, } from '@sapiom/agent'; | ||
| import * as esbuild from 'esbuild'; | ||
| import { AgentOperationError } from './errors.js'; | ||
| function runTypecheck(sourceDir) { | ||
| const tscBin = path.join(sourceDir, 'node_modules', '.bin', 'tsc'); | ||
| import { execFileSync } from "node:child_process"; | ||
| import { createHash } from "node:crypto"; | ||
| import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; | ||
| import { tmpdir } from "node:os"; | ||
| import path from "node:path"; | ||
| import { assertValidGraph, buildManifest, isAgentDefinition, isLegacyOrchestrationDefinition, agentManifestSchema, } from "@sapiom/agent"; | ||
| import * as esbuild from "esbuild"; | ||
| import { AgentOperationError } from "./errors.js"; | ||
| export function runTypecheck(sourceDir) { | ||
| const tscBin = path.join(sourceDir, "node_modules", ".bin", "tsc"); | ||
| if (!existsSync(tscBin)) { | ||
| return 'typecheck skipped — TypeScript is not installed (run npm install first)'; | ||
| return "typecheck skipped — TypeScript is not installed (run npm install first)"; | ||
| } | ||
| try { | ||
| execFileSync(tscBin, ['--noEmit'], { cwd: sourceDir, stdio: ['ignore', 'pipe', 'pipe'] }); | ||
| execFileSync(tscBin, ["--noEmit"], { | ||
| cwd: sourceDir, | ||
| stdio: ["ignore", "pipe", "pipe"], | ||
| }); | ||
| return null; | ||
@@ -20,11 +23,12 @@ } | ||
| const e = err; | ||
| const output = (e.stdout?.toString() ?? '').trim() || (e.stderr?.toString() ?? '').trim(); | ||
| const output = (e.stdout?.toString() ?? "").trim() || | ||
| (e.stderr?.toString() ?? "").trim(); | ||
| throw new AgentOperationError({ | ||
| code: 'TYPECHECK_FAILED', | ||
| message: 'The agent has type errors.', | ||
| hint: output || 'Run `tsc --noEmit` for details.', | ||
| code: "TYPECHECK_FAILED", | ||
| message: "The agent has type errors.", | ||
| hint: output || "Run `tsc --noEmit` for details.", | ||
| }); | ||
| } | ||
| } | ||
| const LOCAL_SDK_VERSION = '0.0.0-local'; | ||
| const LOCAL_SDK_VERSION = "0.0.0-local"; | ||
| export function entryInputSchemaWarning(manifest) { | ||
@@ -38,9 +42,9 @@ const entryStep = manifest.steps[manifest.entry]; | ||
| export async function check(opts) { | ||
| const { sourceDir } = opts; | ||
| const entryFile = path.join(sourceDir, 'index.ts'); | ||
| const sourceDir = path.resolve(opts.sourceDir); | ||
| const entryFile = path.join(sourceDir, "index.ts"); | ||
| if (!existsSync(entryFile)) { | ||
| throw new AgentOperationError({ | ||
| code: 'NO_ENTRY', | ||
| code: "NO_ENTRY", | ||
| message: `No index.ts found in ${sourceDir}.`, | ||
| hint: 'Run this from an agent project, or pass its directory.', | ||
| hint: "Run this from an agent project, or pass its directory.", | ||
| }); | ||
@@ -54,4 +58,4 @@ } | ||
| } | ||
| const tmp = mkdtempSync(path.join(tmpdir(), 'sapiom-check-')); | ||
| const bundlePath = path.join(tmp, 'definition.mjs'); | ||
| const tmp = mkdtempSync(path.join(tmpdir(), "sapiom-check-")); | ||
| const bundlePath = path.join(tmp, "definition.mjs"); | ||
| try { | ||
@@ -63,6 +67,6 @@ try { | ||
| bundle: true, | ||
| platform: 'node', | ||
| target: 'node20', | ||
| format: 'esm', | ||
| logLevel: 'silent', | ||
| platform: "node", | ||
| target: "node20", | ||
| format: "esm", | ||
| logLevel: "silent", | ||
| }); | ||
@@ -72,4 +76,4 @@ } | ||
| throw new AgentOperationError({ | ||
| code: 'BUNDLE_FAILED', | ||
| message: 'Failed to bundle the agent.', | ||
| code: "BUNDLE_FAILED", | ||
| message: "Failed to bundle the agent.", | ||
| hint: err instanceof Error ? err.message : String(err), | ||
@@ -81,3 +85,4 @@ }); | ||
| for (const value of Object.values(mod)) { | ||
| if ((isAgentDefinition(value) || isLegacyOrchestrationDefinition(value)) && !defs.includes(value)) { | ||
| if ((isAgentDefinition(value) || isLegacyOrchestrationDefinition(value)) && | ||
| !defs.includes(value)) { | ||
| defs.push(value); | ||
@@ -88,5 +93,5 @@ } | ||
| throw new AgentOperationError({ | ||
| code: 'NO_DEFINITION', | ||
| message: 'No agent was exported from index.ts.', | ||
| hint: 'Export the result of defineAgent({ … }).', | ||
| code: "NO_DEFINITION", | ||
| message: "No agent was exported from index.ts.", | ||
| hint: "Export the result of defineAgent({ … }).", | ||
| }); | ||
@@ -96,17 +101,22 @@ } | ||
| throw new AgentOperationError({ | ||
| code: 'MULTIPLE_DEFINITIONS', | ||
| message: 'index.ts exports more than one agent.', | ||
| hint: 'Export exactly one defineAgent({ … }) result.', | ||
| code: "MULTIPLE_DEFINITIONS", | ||
| message: "index.ts exports more than one agent.", | ||
| hint: "Export exactly one defineAgent({ … }) result.", | ||
| }); | ||
| } | ||
| const def = defs[0]; | ||
| const sha256 = createHash('sha256').update(readFileSync(bundlePath)).digest('hex'); | ||
| const sha256 = createHash("sha256") | ||
| .update(readFileSync(bundlePath)) | ||
| .digest("hex"); | ||
| let manifest; | ||
| try { | ||
| manifest = agentManifestSchema.parse(buildManifest(def, { sdkVersion: LOCAL_SDK_VERSION, artifact: { sha256, entryFile: 'definition.mjs' } })); | ||
| manifest = agentManifestSchema.parse(buildManifest(def, { | ||
| sdkVersion: LOCAL_SDK_VERSION, | ||
| artifact: { sha256, entryFile: "definition.mjs" }, | ||
| })); | ||
| } | ||
| catch (err) { | ||
| throw new AgentOperationError({ | ||
| code: 'MANIFEST_INVALID', | ||
| message: 'The agent produced an invalid manifest.', | ||
| code: "MANIFEST_INVALID", | ||
| message: "The agent produced an invalid manifest.", | ||
| hint: err instanceof Error ? err.message : String(err), | ||
@@ -120,4 +130,4 @@ }); | ||
| throw new AgentOperationError({ | ||
| code: 'GRAPH_INVALID', | ||
| message: 'The agent graph is invalid.', | ||
| code: "GRAPH_INVALID", | ||
| message: "The agent graph is invalid.", | ||
| hint: err instanceof Error ? err.message : String(err), | ||
@@ -130,4 +140,6 @@ }); | ||
| const steps = manifest.steps; | ||
| const stepCount = Array.isArray(steps) ? steps.length : Object.keys(steps ?? {}).length; | ||
| const name = manifest.name ?? 'agent'; | ||
| const stepCount = Array.isArray(steps) | ||
| ? steps.length | ||
| : Object.keys(steps ?? {}).length; | ||
| const name = manifest.name ?? "agent"; | ||
| return { name, stepCount, warnings, manifest }; | ||
@@ -134,0 +146,0 @@ } |
@@ -1,3 +0,3 @@ | ||
| import { GatewayClient } from './client.js'; | ||
| import { type CloneRepoOptions } from './git.js'; | ||
| import { GatewayClient } from "./client.js"; | ||
| import { type CloneRepoOptions } from "./git.js"; | ||
| export interface CloneOptions { | ||
@@ -4,0 +4,0 @@ templateId?: string; |
+88
-25
@@ -1,6 +0,17 @@ | ||
| import { existsSync, mkdirSync, readdirSync } from 'node:fs'; | ||
| import path from 'node:path'; | ||
| import { writeConfig } from './config.js'; | ||
| import { AgentOperationError } from './errors.js'; | ||
| import { cloneRepo as defaultCloneRepo } from './git.js'; | ||
| import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, renameSync, rmSync, } from "node:fs"; | ||
| import path from "node:path"; | ||
| import { writeConfig } from "./config.js"; | ||
| import { AgentOperationError } from "./errors.js"; | ||
| import { cloneRepo as defaultCloneRepo } from "./git.js"; | ||
| const CLONE_PROPAGATION_MAX_ATTEMPTS = 8; | ||
| const CLONE_PROPAGATION_DELAY_MS = 500; | ||
| const CLONE_PROPAGATION_ERROR = /remote branch .* not found|couldn't find remote ref|repository appears to be empty/i; | ||
| function isClonePropagationError(error) { | ||
| return (error instanceof AgentOperationError && | ||
| error.code === "GIT_CLONE" && | ||
| CLONE_PROPAGATION_ERROR.test(error.hint ?? "")); | ||
| } | ||
| function delay(ms) { | ||
| return new Promise((resolve) => setTimeout(resolve, ms)); | ||
| } | ||
| export async function clone(opts, client) { | ||
@@ -12,4 +23,4 @@ const { templateId, forkId, definitionId, targetDir } = opts; | ||
| throw new AgentOperationError({ | ||
| code: 'BAD_INPUT', | ||
| message: 'Provide a templateId (to fork then clone), a forkId (to clone an existing fork), or a definitionId (to clone a deployed agent).', | ||
| code: "BAD_INPUT", | ||
| message: "Provide a templateId (to fork then clone), a forkId (to clone an existing fork), or a definitionId (to clone a deployed agent).", | ||
| }); | ||
@@ -19,12 +30,21 @@ } | ||
| throw new AgentOperationError({ | ||
| code: 'BAD_INPUT', | ||
| message: 'Provide only one of templateId, forkId, or definitionId.', | ||
| hint: 'Use templateId to start from a gallery template, forkId to re-clone an existing fork, or definitionId to pull a deployed agent locally.', | ||
| code: "BAD_INPUT", | ||
| message: "Provide only one of templateId, forkId, or definitionId.", | ||
| hint: "Use templateId to start from a gallery template, forkId to re-clone an existing fork, or definitionId to pull a deployed agent locally.", | ||
| }); | ||
| } | ||
| if (existsSync(targetDir) && readdirSync(targetDir).length > 0) { | ||
| throw new AgentOperationError({ | ||
| code: 'DIR_NOT_EMPTY', | ||
| message: `Target directory '${targetDir}' already exists and is not empty.`, | ||
| }); | ||
| let preserveStudioState = false; | ||
| if (existsSync(targetDir)) { | ||
| const entries = readdirSync(targetDir); | ||
| if (entries.length === 1 && entries[0] === ".sapiom") { | ||
| const studioState = lstatSync(path.join(targetDir, ".sapiom")); | ||
| preserveStudioState = | ||
| studioState.isDirectory() && !studioState.isSymbolicLink(); | ||
| } | ||
| if (entries.length > 0 && !preserveStudioState) { | ||
| throw new AgentOperationError({ | ||
| code: "DIR_NOT_EMPTY", | ||
| message: `Target directory '${targetDir}' already exists and is not empty.`, | ||
| }); | ||
| } | ||
| } | ||
@@ -41,6 +61,6 @@ let resolvedForkId = forkId; | ||
| : await client.post(`/forks/${encodeURIComponent(resolvedForkId)}/clone-token`, {}); | ||
| if (!token.cloneUrl.startsWith('https://')) { | ||
| if (!token.cloneUrl.startsWith("https://")) { | ||
| throw new AgentOperationError({ | ||
| code: 'BAD_CLONE_URL', | ||
| message: 'The clone token endpoint returned an unexpected clone URL.', | ||
| code: "BAD_CLONE_URL", | ||
| message: "The clone token endpoint returned an unexpected clone URL.", | ||
| }); | ||
@@ -50,9 +70,52 @@ } | ||
| mkdirSync(parent, { recursive: true }); | ||
| runClone({ | ||
| cloneUrl: token.cloneUrl, | ||
| targetDir, | ||
| branch: token.defaultBranch, | ||
| repoFullName: token.repoFullName, | ||
| cwd: parent, | ||
| }); | ||
| let stagedRoot = null; | ||
| let cloneTarget = null; | ||
| try { | ||
| for (let attempt = 1; attempt <= CLONE_PROPAGATION_MAX_ATTEMPTS; attempt++) { | ||
| stagedRoot = mkdtempSync(path.join(parent, ".sapiom-clone-")); | ||
| cloneTarget = stagedRoot; | ||
| try { | ||
| runClone({ | ||
| cloneUrl: token.cloneUrl, | ||
| targetDir: cloneTarget, | ||
| branch: token.defaultBranch, | ||
| repoFullName: token.repoFullName, | ||
| cwd: parent, | ||
| }); | ||
| break; | ||
| } | ||
| catch (error) { | ||
| rmSync(stagedRoot, { recursive: true, force: true }); | ||
| stagedRoot = null; | ||
| cloneTarget = null; | ||
| if (attempt === CLONE_PROPAGATION_MAX_ATTEMPTS || | ||
| !isClonePropagationError(error)) { | ||
| throw error; | ||
| } | ||
| await delay(CLONE_PROPAGATION_DELAY_MS); | ||
| } | ||
| } | ||
| if (!cloneTarget) { | ||
| throw new AgentOperationError({ | ||
| code: "GIT_CLONE", | ||
| message: "git clone failed.", | ||
| }); | ||
| } | ||
| const clonedEntries = readdirSync(cloneTarget); | ||
| if (clonedEntries.includes(".sapiom")) { | ||
| throw new AgentOperationError({ | ||
| code: "STUDIO_STATE_CONFLICT", | ||
| message: "The cloned repository contains a reserved .sapiom directory.", | ||
| hint: "Remove .sapiom from the template repository, then try again.", | ||
| }); | ||
| } | ||
| mkdirSync(targetDir, { recursive: true }); | ||
| for (const entry of clonedEntries) { | ||
| renameSync(path.join(cloneTarget, entry), path.join(targetDir, entry)); | ||
| } | ||
| } | ||
| finally { | ||
| if (stagedRoot) | ||
| rmSync(stagedRoot, { recursive: true, force: true }); | ||
| } | ||
| writeConfig(targetDir, { | ||
@@ -59,0 +122,0 @@ repoFullName: token.repoFullName, |
@@ -1,2 +0,2 @@ | ||
| import { GatewayClient } from './client.js'; | ||
| import { GatewayClient } from "./client.js"; | ||
| export interface DeployOptions { | ||
@@ -3,0 +3,0 @@ projectDir: string; |
+41
-36
@@ -1,18 +0,18 @@ | ||
| import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; | ||
| import { tmpdir } from 'node:os'; | ||
| import path from 'node:path'; | ||
| import { getOrchestrationAnalytics, telemetryErrorCode } from './analytics.js'; | ||
| import { bundleForDeploy } from './bundle.js'; | ||
| import { AgentOperationError } from './errors.js'; | ||
| import { assertDeployable, pushHead, pushSynthesizedTree } from './git.js'; | ||
| import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; | ||
| import { tmpdir } from "node:os"; | ||
| import path from "node:path"; | ||
| import { getOrchestrationAnalytics, telemetryErrorCode } from "./analytics.js"; | ||
| import { bundleForDeploy } from "./bundle.js"; | ||
| import { AgentOperationError } from "./errors.js"; | ||
| import { assertDeployable, pushHead, pushSynthesizedTree } from "./git.js"; | ||
| function isPushAuthError(err) { | ||
| if (!(err instanceof AgentOperationError) || err.code !== 'GIT') | ||
| if (!(err instanceof AgentOperationError) || err.code !== "GIT") | ||
| return false; | ||
| const text = ((err.hint ?? '') + ' ' + err.message).toLowerCase(); | ||
| return (text.includes('authentication failed') || | ||
| text.includes('could not read from') || | ||
| text.includes('the requested url returned error: 401') || | ||
| text.includes('the requested url returned error: 403')); | ||
| const text = ((err.hint ?? "") + " " + err.message).toLowerCase(); | ||
| return (text.includes("authentication failed") || | ||
| text.includes("could not read from") || | ||
| text.includes("the requested url returned error: 401") || | ||
| text.includes("the requested url returned error: 403")); | ||
| } | ||
| const TERMINAL = new Set(['ready', 'failed', 'cancelled', 'superseded']); | ||
| const TERMINAL = new Set(["ready", "failed", "cancelled", "superseded"]); | ||
| const POLL_DELAYS_MS = [1000, 2000, 3000, 5000, 5000, 8000, 10000]; | ||
@@ -25,8 +25,8 @@ const POLL_BUDGET_MS = 300000; | ||
| const result = await deployOperation(opts, client); | ||
| getOrchestrationAnalytics().track('workflow.deploy', { | ||
| getOrchestrationAnalytics().track("workflow.deploy", { | ||
| workflow_id: opts.definitionId, | ||
| branch: opts.branch ?? 'main', | ||
| branch: opts.branch ?? "main", | ||
| build_run_id: result.buildRunId, | ||
| build_status: result.status, | ||
| status: 'success', | ||
| status: "success", | ||
| duration_ms: Date.now() - startedAt, | ||
@@ -37,6 +37,6 @@ }); | ||
| catch (err) { | ||
| getOrchestrationAnalytics().track('workflow.deploy', { | ||
| getOrchestrationAnalytics().track("workflow.deploy", { | ||
| workflow_id: opts.definitionId, | ||
| branch: opts.branch ?? 'main', | ||
| status: 'error', | ||
| branch: opts.branch ?? "main", | ||
| status: "error", | ||
| error_code: telemetryErrorCode(err), | ||
@@ -49,10 +49,15 @@ duration_ms: Date.now() - startedAt, | ||
| async function deployOperation(opts, client) { | ||
| const { projectDir, definitionId, branch = 'main' } = opts; | ||
| const { projectDir, definitionId, branch = "main" } = opts; | ||
| assertDeployable(projectDir); | ||
| const { code, dependencies } = await bundleForDeploy(projectDir); | ||
| const { pushUrl } = await client.post(`/definitions/${definitionId}/push-credentials`, {}); | ||
| const treeDir = mkdtempSync(path.join(tmpdir(), 'sapiom-deploy-')); | ||
| const treeDir = mkdtempSync(path.join(tmpdir(), "sapiom-deploy-")); | ||
| try { | ||
| writeFileSync(path.join(treeDir, 'index.ts'), code); | ||
| writeFileSync(path.join(treeDir, 'package.json'), JSON.stringify({ name: 'agent-definition', private: true, type: 'module', dependencies }, null, 2) + '\n'); | ||
| writeFileSync(path.join(treeDir, "index.ts"), code); | ||
| writeFileSync(path.join(treeDir, "package.json"), JSON.stringify({ | ||
| name: "agent-definition", | ||
| private: true, | ||
| type: "module", | ||
| dependencies, | ||
| }, null, 2) + "\n"); | ||
| try { | ||
@@ -75,19 +80,19 @@ pushSynthesizedTree(treeDir, pushUrl, branch); | ||
| throw new AgentOperationError({ | ||
| code: 'BUILD_NO_ID', | ||
| message: 'The build was triggered but no build id was returned.', | ||
| code: "BUILD_NO_ID", | ||
| message: "The build was triggered but no build id was returned.", | ||
| }); | ||
| } | ||
| const final = await pollBuild(client, definitionId, buildRunId); | ||
| if (final.status !== 'ready') { | ||
| if (final.status === 'superseded') { | ||
| if (final.status !== "ready") { | ||
| if (final.status === "superseded") { | ||
| throw new AgentOperationError({ | ||
| code: 'BUILD_SUPERSEDED', | ||
| message: 'A newer deploy superseded this build.', | ||
| step: 'build', | ||
| code: "BUILD_SUPERSEDED", | ||
| message: "A newer deploy superseded this build.", | ||
| step: "build", | ||
| }); | ||
| } | ||
| throw new AgentOperationError({ | ||
| code: 'BUILD_FAILED', | ||
| code: "BUILD_FAILED", | ||
| message: `Build ${final.status}.`, | ||
| step: 'build', | ||
| step: "build", | ||
| hint: final.error?.stack || final.error?.message, | ||
@@ -110,7 +115,7 @@ }); | ||
| throw new AgentOperationError({ | ||
| code: 'BUILD_TIMEOUT', | ||
| message: 'Build did not finish in time.', | ||
| step: 'build', | ||
| code: "BUILD_TIMEOUT", | ||
| message: "Build did not finish in time.", | ||
| step: "build", | ||
| hint: `Check it later via the logs API for build ${buildRunId}`, | ||
| }); | ||
| } |
+46
-20
@@ -1,6 +0,10 @@ | ||
| import { execFileSync } from 'node:child_process'; | ||
| import { AgentOperationError } from './errors.js'; | ||
| import { execFileSync } from "node:child_process"; | ||
| import { AgentOperationError } from "./errors.js"; | ||
| function git(args, cwd) { | ||
| try { | ||
| return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); | ||
| return execFileSync("git", args, { | ||
| cwd, | ||
| encoding: "utf8", | ||
| stdio: ["ignore", "pipe", "pipe"], | ||
| }).trim(); | ||
| } | ||
@@ -11,3 +15,3 @@ catch (err) { | ||
| throw new AgentOperationError({ | ||
| code: 'GIT', | ||
| code: "GIT", | ||
| message: `git ${args[0]} failed.`, | ||
@@ -20,8 +24,11 @@ hint, | ||
| try { | ||
| execFileSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: dir, stdio: 'ignore' }); | ||
| execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { | ||
| cwd: dir, | ||
| stdio: "ignore", | ||
| }); | ||
| } | ||
| catch { | ||
| throw new AgentOperationError({ | ||
| code: 'NOT_GIT', | ||
| message: 'Not a git repository.', | ||
| code: "NOT_GIT", | ||
| message: "Not a git repository.", | ||
| hint: 'Initialize one: git init && git add -A && git commit -m "init"', | ||
@@ -31,8 +38,8 @@ }); | ||
| try { | ||
| execFileSync('git', ['rev-parse', 'HEAD'], { cwd: dir, stdio: 'ignore' }); | ||
| execFileSync("git", ["rev-parse", "HEAD"], { cwd: dir, stdio: "ignore" }); | ||
| } | ||
| catch { | ||
| throw new AgentOperationError({ | ||
| code: 'NO_COMMITS', | ||
| message: 'This repository has no commits yet.', | ||
| code: "NO_COMMITS", | ||
| message: "This repository has no commits yet.", | ||
| hint: 'Commit your work: git add -A && git commit -m "…"', | ||
@@ -43,3 +50,3 @@ }); | ||
| export function redactCredentials(text) { | ||
| return text.replace(/(https?:\/\/)[^@\s/]+@/gi, '$1***@'); | ||
| return text.replace(/(https?:\/\/)[^@\s/]+@/gi, "$1***@"); | ||
| } | ||
@@ -49,3 +56,13 @@ export function cloneRepo(opts) { | ||
| try { | ||
| execFileSync('git', ['clone', '--depth', '1', '--single-branch', '--branch', branch, '--', cloneUrl, targetDir], { cwd, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' }); | ||
| execFileSync("git", [ | ||
| "clone", | ||
| "--depth", | ||
| "1", | ||
| "--single-branch", | ||
| "--branch", | ||
| branch, | ||
| "--", | ||
| cloneUrl, | ||
| targetDir, | ||
| ], { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" }); | ||
| } | ||
@@ -56,4 +73,4 @@ catch (err) { | ||
| throw new AgentOperationError({ | ||
| code: 'GIT_CLONE', | ||
| message: 'git clone failed.', | ||
| code: "GIT_CLONE", | ||
| message: "git clone failed.", | ||
| hint: redactCredentials(raw), | ||
@@ -63,3 +80,3 @@ }); | ||
| try { | ||
| git(['remote', 'set-url', 'origin', `https://github.com/${repoFullName}.git`], targetDir); | ||
| git(["remote", "set-url", "origin", `https://github.com/${repoFullName}.git`], targetDir); | ||
| } | ||
@@ -70,9 +87,18 @@ catch { | ||
| export function pushHead(dir, pushUrl, branch) { | ||
| git(['push', '--force', pushUrl, `HEAD:${branch}`], dir); | ||
| git(["push", "--force", pushUrl, `HEAD:${branch}`], dir); | ||
| } | ||
| export function pushSynthesizedTree(treeDir, pushUrl, branch) { | ||
| git(['init', '-q', '-b', branch], treeDir); | ||
| git(['add', '-A'], treeDir); | ||
| git(['-c', 'user.email=deploy@sapiom.ai', '-c', 'user.name=Sapiom Deploy', 'commit', '-q', '-m', 'deploy'], treeDir); | ||
| git(['push', '--force', pushUrl, `HEAD:${branch}`], treeDir); | ||
| git(["init", "-q", "-b", branch], treeDir); | ||
| git(["add", "-A"], treeDir); | ||
| git([ | ||
| "-c", | ||
| "user.email=deploy@sapiom.ai", | ||
| "-c", | ||
| "user.name=Sapiom Deploy", | ||
| "commit", | ||
| "-q", | ||
| "-m", | ||
| "deploy", | ||
| ], treeDir); | ||
| git(["push", "--force", pushUrl, `HEAD:${branch}`], treeDir); | ||
| } |
@@ -32,5 +32,5 @@ export { AgentOperationError } from "./errors.js"; | ||
| export type { SendFeedbackOptions, SendFeedbackResult } from "./feedback.js"; | ||
| export { createSchedule, listSchedules, getSchedule, cancelSchedule, previewCron } from "./schedule.js"; | ||
| export { createSchedule, listSchedules, getSchedule, cancelSchedule, previewCron, } from "./schedule.js"; | ||
| export type { ScheduleKind, ScheduleStatus, SchedulePolicy, CreateScheduleOptions, ListSchedulesOptions, CronPreviewOptions, CronPreview, ScheduleSummary, ScheduleDetail, ScheduleFireRecord, } from "./schedule.js"; | ||
| export { assertDeployable, pushHead, cloneRepo, redactCredentials } from "./git.js"; | ||
| export { assertDeployable, pushHead, cloneRepo, redactCredentials, } from "./git.js"; | ||
| export type { CloneRepoOptions } from "./git.js"; | ||
@@ -37,0 +37,0 @@ export { parseStubFile, STUB_FILE_VERSION } from "./local/stubs.js"; |
@@ -17,4 +17,4 @@ export { AgentOperationError } from "./errors.js"; | ||
| export { sendFeedback } from "./feedback.js"; | ||
| export { createSchedule, listSchedules, getSchedule, cancelSchedule, previewCron } from "./schedule.js"; | ||
| export { assertDeployable, pushHead, cloneRepo, redactCredentials } from "./git.js"; | ||
| export { createSchedule, listSchedules, getSchedule, cancelSchedule, previewCron, } from "./schedule.js"; | ||
| export { assertDeployable, pushHead, cloneRepo, redactCredentials, } from "./git.js"; | ||
| export { parseStubFile, STUB_FILE_VERSION } from "./local/stubs.js"; | ||
@@ -21,0 +21,0 @@ export { runLocal, runLocalFromDir, STUBS_FILE } from "./local/run-local.js"; |
@@ -1,2 +0,2 @@ | ||
| import { InMemoryContextStore, } from "@sapiom/agent"; | ||
| import { InMemoryContextStore, StepInputValidationError, } from "@sapiom/agent"; | ||
| import { parseCorrelationId, STEP_COMPLETION_OUTCOME, } from "@sapiom/agent-runtime"; | ||
@@ -60,5 +60,13 @@ import { createStubClient } from "@sapiom/tools/stub"; | ||
| }; | ||
| let stepInput = request.input; | ||
| let directive; | ||
| try { | ||
| directive = await step.run(request.input, ctx); | ||
| if (step.inputSchema) { | ||
| const parsedInput = step.inputSchema.safeParse(request.input); | ||
| if (!parsedInput.success) { | ||
| throw new StepInputValidationError(request.stepName, parsedInput.error.issues); | ||
| } | ||
| stepInput = parsedInput.data; | ||
| } | ||
| directive = await step.run(stepInput, ctx); | ||
| } | ||
@@ -70,3 +78,3 @@ catch (err) { | ||
| attempt: request.attempt, | ||
| input: request.input, | ||
| input: e instanceof StepInputValidationError ? request.input : stepInput, | ||
| status: "threw", | ||
@@ -92,3 +100,3 @@ error: { name: e.name, message: e.message, stack: e.stack }, | ||
| attempt: request.attempt, | ||
| input: request.input, | ||
| input: stepInput, | ||
| status: "succeeded", | ||
@@ -95,0 +103,0 @@ output, |
| export declare const STUB_FILE_VERSION = 1; | ||
| export type StubResponse = unknown; | ||
| export type StepStubs = Record<string, StubResponse | StubResponse[]>; | ||
| export type StepStubs = Record<string, StubResponse>; | ||
| export interface StubFile { | ||
@@ -5,0 +5,0 @@ version: number; |
+14
-10
@@ -1,19 +0,23 @@ | ||
| import { AgentOperationError } from '../errors.js'; | ||
| import { AgentOperationError } from "../errors.js"; | ||
| export const STUB_FILE_VERSION = 1; | ||
| export function parseStubFile(raw) { | ||
| if (raw == null || typeof raw !== 'object' || Array.isArray(raw)) { | ||
| throw invalid('the stub file must be a JSON object.'); | ||
| if (raw == null || typeof raw !== "object" || Array.isArray(raw)) { | ||
| throw invalid("the stub file must be a JSON object."); | ||
| } | ||
| const obj = raw; | ||
| const version = obj.version ?? STUB_FILE_VERSION; | ||
| if (typeof version !== 'number') { | ||
| throw invalid('`version` must be a number.'); | ||
| if (typeof version !== "number") { | ||
| throw invalid("`version` must be a number."); | ||
| } | ||
| const stepsRaw = obj.steps ?? {}; | ||
| if (stepsRaw == null || typeof stepsRaw !== 'object' || Array.isArray(stepsRaw)) { | ||
| throw invalid('`steps` must be an object keyed by step name.'); | ||
| if (stepsRaw == null || | ||
| typeof stepsRaw !== "object" || | ||
| Array.isArray(stepsRaw)) { | ||
| throw invalid("`steps` must be an object keyed by step name."); | ||
| } | ||
| const steps = {}; | ||
| for (const [stepName, stepStubs] of Object.entries(stepsRaw)) { | ||
| if (stepStubs == null || typeof stepStubs !== 'object' || Array.isArray(stepStubs)) { | ||
| if (stepStubs == null || | ||
| typeof stepStubs !== "object" || | ||
| Array.isArray(stepStubs)) { | ||
| throw invalid(`steps.${stepName} must be an object keyed by capability path.`); | ||
@@ -27,6 +31,6 @@ } | ||
| return new AgentOperationError({ | ||
| code: 'STUBS_INVALID', | ||
| code: "STUBS_INVALID", | ||
| message: `Invalid stub file: ${detail}`, | ||
| hint: 'Expected { "version": 1, "steps": { "<step>": { "<capability.path>": <response> | [<response>] } } }.', | ||
| hint: 'Expected { "version": 1, "steps": { "<step>": { "<capability.path>": <response> } } }.', | ||
| }); | ||
| } |
| export declare const VERSION_FALLBACK: { | ||
| readonly agent: "0.9.3"; | ||
| readonly tools: "0.26.0"; | ||
| readonly tools: "0.26.1"; | ||
| }; |
| export const VERSION_FALLBACK = { | ||
| agent: "0.9.3", | ||
| tools: "0.26.0", | ||
| tools: "0.26.1", | ||
| }; |
+4
-3
| { | ||
| "name": "@sapiom/agent-core", | ||
| "version": "0.10.2", | ||
| "version": "0.10.3", | ||
| "description": "Pure, stateless core functions for scaffolding, validating, and operating Sapiom agents — shared by the CLI and MCP packages.", | ||
@@ -43,3 +43,3 @@ "license": "MIT", | ||
| "@sapiom/analytics-core": "^0.2.1", | ||
| "@sapiom/tools": "^0.26.0" | ||
| "@sapiom/tools": "^0.26.1" | ||
| }, | ||
@@ -55,3 +55,4 @@ "devDependencies": { | ||
| "ts-jest": "^29.1.2", | ||
| "typescript": "^5.4.2" | ||
| "typescript": "^5.4.2", | ||
| "zod": "^3.25.0" | ||
| }, | ||
@@ -58,0 +59,0 @@ "engines": { |
@@ -19,4 +19,5 @@ --- | ||
| paid Sapiom capabilities through the typed `ctx.sapiom.*` client — and returns a directive. | ||
| You test it locally for free, then deploy it to run on Sapiom's cloud: on demand, on a | ||
| schedule, or resumed by signals. All from the terminal; no dashboard required. | ||
| You test it locally without a Sapiom account or capability spend, then deploy it to run on | ||
| Sapiom's cloud: on demand, on a schedule, or resumed by signals. All from the terminal; no | ||
| dashboard required. | ||
@@ -40,10 +41,4 @@ **Load this skill before scaffolding — it drives the whole lifecycle from zero.** Inside a | ||
| ### 1. Authenticate (required before deploy/run) | ||
| ### 1. Scaffold | ||
| Run `sapiom_authenticate` — it opens a browser login and caches an API key in | ||
| `~/.sapiom/credentials.json`. Confirm with `sapiom_status`. This makes your coding agent an | ||
| API-key principal; deployed agents inherit that authority to call paid capabilities. | ||
| ### 2. Scaffold | ||
| Call `sapiom_dev_agents_scaffold` with a target directory. The scaffold writes: | ||
@@ -64,19 +59,29 @@ | ||
| ### 3. Write steps → typecheck → check → run_local → deploy | ||
| ### 2. Write steps → typecheck → check → run_local | ||
| | Command | What it does | | ||
| |---|---| | ||
| | `npm run typecheck` | Confirms types compile and every `ctx.sapiom.*` call exists | | ||
| | `sapiom_dev_agents_check` | Bundles `index.ts` + validates the step graph (offline, instant) | | ||
| | `sapiom_dev_agents_run_local` | Runs real step code with all capabilities stubbed — free, no spend | | ||
| | Command | What it does | | ||
| | ----------------------------- | -------------------------------------------------------------------------------------------------------------------- | | ||
| | `npm run typecheck` | Confirms types compile and every `ctx.sapiom.*` call exists | | ||
| | `sapiom_dev_agents_check` | Typechecks, bundles and imports `index.ts`, then validates the manifest and graph; no Sapiom account or service call | | ||
| | `sapiom_dev_agents_run_local` | Runs real step code with `ctx.sapiom.*` calls stubbed — no Sapiom capability spend | | ||
| Then ship: | ||
| `check` imports your definition, and `run_local` executes your real step bodies. Neither | ||
| contacts a Sapiom service, but author-written top-level or step code can still use the local | ||
| filesystem, process, environment, network, and third-party services. | ||
| | Command | What it does | | ||
| |---|---| | ||
| | `sapiom_dev_agents_link` | Registers the agent under your tenant | | ||
| | `sapiom_dev_agents_deploy` | Builds and deploys to Sapiom's cloud | | ||
| | `sapiom_dev_agents_run` | Starts a real (billed) execution | | ||
| | `sapiom_dev_agents_inspect` | Watch an execution's status, steps, and spend | | ||
| ### 3. Authenticate before cloud work | ||
| Run `sapiom_authenticate` — it opens a browser login and caches an API key in | ||
| `~/.sapiom/credentials.json`. Confirm with `sapiom_status`. This makes your coding agent an | ||
| API-key principal; link, deploy, and cloud run require it. | ||
| ### 4. Link → deploy → run → inspect | ||
| | Command | What it does | | ||
| | --------------------------- | --------------------------------------------------- | | ||
| | `sapiom_dev_agents_link` | Registers the agent under your tenant | | ||
| | `sapiom_dev_agents_deploy` | Builds and deploys to Sapiom's cloud | | ||
| | `sapiom_dev_agents_run` | Starts a real (billed) execution | | ||
| | `sapiom_dev_agents_inspect` | Watch status, pinned build, steps, logs, and output | | ||
| ## The Step Model — Hard Rules | ||
@@ -86,9 +91,9 @@ | ||
| | Import | From | | ||
| |---|---| | ||
| | `defineAgent` | `@sapiom/agent` | | ||
| | `defineStep` | `@sapiom/agent` | | ||
| | Import | From | | ||
| | ---------------------------------------------------- | --------------- | | ||
| | `defineAgent` | `@sapiom/agent` | | ||
| | `defineStep` | `@sapiom/agent` | | ||
| | `goto / terminate / fail / retry / pauseUntilSignal` | `@sapiom/agent` | | ||
| | `AgentExecutionContext` | `@sapiom/agent` | | ||
| | `CODING_RESULT_SIGNAL / CodingResultPayload` | `@sapiom/tools` | | ||
| | `AgentExecutionContext` | `@sapiom/agent` | | ||
| | `CODING_RESULT_SIGNAL / CodingResultPayload` | `@sapiom/tools` | | ||
@@ -101,4 +106,4 @@ `@sapiom/agent` is the only authoring package. | ||
| export const agent = defineAgent({ | ||
| name: "my-agent", // string — used for logging and inspect | ||
| entry: "start", // must name a key in steps | ||
| name: "my-agent", // string — used for logging and inspect | ||
| entry: "start", // must name a key in steps | ||
| steps: { start, finish }, | ||
@@ -112,12 +117,12 @@ }); | ||
| | Field | Type | Required | Notes | | ||
| |---|---|---|---| | ||
| | `name` | `string` | yes | Step's id; must match its key in the steps object | | ||
| | `next` | `readonly string[]` | yes | Step names this step may `goto`. Empty array if terminal | | ||
| | `terminal` | `boolean` | no | `true` if this step ends the agent's execution | | ||
| | `canFail` | `boolean` | no | Must be `true` to return `fail()` | | ||
| | `pause` | `{ signal, resumeStep }` | no | Required when returning `pauseUntilSignal(...)` | | ||
| | `inputSchema` | `ZodType` | no | Zod schema validating this step's input. On the **entry** step it is the agent's public API (see [The Entry Input Contract](#the-entry-input-contract--your-agents-public-api)) | | ||
| | `timeoutMs` | `number` | no | Per-step timeout; no automatic retry cap | | ||
| | `run(input, ctx)` | `async function` | yes | Returns a directive | | ||
| | Field | Type | Required | Notes | | ||
| | ----------------- | ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| | `name` | `string` | yes | Step's id; must match its key in the steps object | | ||
| | `next` | `readonly string[]` | yes | Step names this step may `goto`. Empty array if terminal | | ||
| | `terminal` | `boolean` | no | `true` if this step ends the agent's execution | | ||
| | `canFail` | `boolean` | no | Must be `true` to return `fail()` | | ||
| | `pause` | `{ signal, resumeStep }` | no | Required when returning `pauseUntilSignal(...)` | | ||
| | `inputSchema` | `ZodType` | no | Zod schema validating this step's input. On the **entry** step it is the agent's public API (see [The Entry Input Contract](#the-entry-input-contract--your-agents-public-api)) | | ||
| | `timeoutMs` | `number` | no | Per-attempt step timeout; the engine separately caps attempts (three by default) | | ||
| | `run(input, ctx)` | `async function` | yes | Returns a directive | | ||
@@ -129,9 +134,9 @@ Import Zod via the `zod/v4` subpath — `import { z } from "zod/v4"` — to match the SDK's | ||
| | Directive | Function | Constraint | | ||
| |---|---|---| | ||
| | `goto(target, output?)` | Advance to another step | `target` must be in `next[]` | | ||
| | `terminate(output?, opts?)` | End the execution successfully | Step must have `terminal: true` | | ||
| | `fail(reason?, opts?)` | End the execution as failed | Step must have `canFail: true` | | ||
| | `retry(opts?)` | Re-run this step | Bound with `ctx.attempts` — no automatic cap | | ||
| | `pauseUntilSignal(handle, opts?)` | Suspend until a signal fires | Step must declare `pause: { signal, resumeStep }` | | ||
| | Directive | Function | Constraint | | ||
| | --------------------------------- | ------------------------------ | --------------------------------------------------------- | | ||
| | `goto(target, output?)` | Advance to another step | `target` must be in `next[]` | | ||
| | `terminate(output?, opts?)` | End the execution successfully | Step must have `terminal: true` | | ||
| | `fail(reason?, opts?)` | End the execution as failed | Step must have `canFail: true` | | ||
| | `retry(opts?)` | Re-run this step | Explicit retry, capped at three total attempts by default | | ||
| | `pauseUntilSignal(handle, opts?)` | Suspend until a signal fires | Step must declare `pause: { signal, resumeStep }` | | ||
@@ -182,3 +187,3 @@ TypeScript enforces these constraints at compile time — a `terminate` in a non-terminal step, | ||
| Declare it on the entry step even when the agent looks input-free: an entry step with **no** | ||
| `inputSchema` tells the platform the agent takes *no* input, so the dashboard renders an | ||
| `inputSchema` tells the platform the agent takes _no_ input, so the dashboard renders an | ||
| empty Run form and callers have nothing to fill in (and `check` warns). Give every field a | ||
@@ -247,14 +252,14 @@ `.default()` so a zero-input run — the dashboard "Run" button with an empty form — still | ||
| | Field | Type | Notes | | ||
| |---|---|---| | ||
| | `ctx.executionId` | `string` | Unique id for this execution | | ||
| | `ctx.agentName` | `string` | The agent's `name` | | ||
| | `ctx.input` | `unknown` | The execution's entry input — same value the entry step's `run` arg receives. Use `ctx.shared` to carry it forward; don't rely on `ctx.input` downstream. | | ||
| | `ctx.shared` | `TypedContextStore<TShared>` | Cross-step key/value store | | ||
| | `ctx.history` | `readonly StepExecutionRecord[]` | Previous steps' records | | ||
| | `ctx.attempts` | `number` | How many times this step has run (0-indexed) | | ||
| | `ctx.logger` | `StepLogger` | `info / warn / error / debug(msg, meta?)` | | ||
| | `ctx.sapiom` | `Sapiom` | The typed capability client — the `Sapiom` interface from `@sapiom/tools`, installed in your `node_modules` (see "Capabilities" below) | | ||
| | `ctx.organizationId` | `string \| null` | Tenant org | | ||
| | `ctx.tenantId` | `string \| null` | Tenant id | | ||
| | Field | Type | Notes | | ||
| | -------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| | `ctx.executionId` | `string` | Unique id for this execution | | ||
| | `ctx.agentName` | `string` | The agent's `name` | | ||
| | `ctx.input` | `unknown` | The execution's entry input — same value the entry step's `run` arg receives. Use `ctx.shared` to carry it forward; don't rely on `ctx.input` downstream. | | ||
| | `ctx.shared` | `TypedContextStore<TShared>` | Cross-step key/value store | | ||
| | `ctx.history` | `readonly StepExecutionRecord[]` | Previous steps' records | | ||
| | `ctx.attempts` | `number` | How many times this step has run (0-indexed) | | ||
| | `ctx.logger` | `StepLogger` | `info / warn / error / debug(msg, meta?)` | | ||
| | `ctx.sapiom` | `Sapiom` | The typed capability client — the `Sapiom` interface from `@sapiom/tools`, installed in your `node_modules` (see "Capabilities" below) | | ||
| | `ctx.organizationId` | `string \| null` | Tenant org | | ||
| | `ctx.tenantId` | `string \| null` | Tenant id | | ||
@@ -305,4 +310,4 @@ ## Capabilities from Steps | ||
| `timeoutMs` on a step caps how long its `run` may take. There is no engine-level retry cap — | ||
| you own the bound. | ||
| `timeoutMs` caps one attempt of a step's `run`. The engine allows three attempts per step by | ||
| default, counting the initial attempt; keep author-controlled retry logic inside that ceiling. | ||
@@ -318,3 +323,7 @@ ## Pause & Resume (Long-Running Dispatched Steps) | ||
| import { | ||
| defineAgent, defineStep, goto, pauseUntilSignal, terminate, | ||
| defineAgent, | ||
| defineStep, | ||
| goto, | ||
| pauseUntilSignal, | ||
| terminate, | ||
| type AgentExecutionContext, | ||
@@ -335,4 +344,7 @@ } from "@sapiom/agent"; | ||
| const repo = await ctx.sapiom.repositories.create("my-repo"); | ||
| ctx.shared.set("repoSlug", repo.slug); // stash before pausing | ||
| const run = await ctx.sapiom.models.coding.launch({ task: input.task, gitRepository: repo }); | ||
| ctx.shared.set("repoSlug", repo.slug); // stash before pausing | ||
| const run = await ctx.sapiom.models.coding.launch({ | ||
| task: input.task, | ||
| gitRepository: repo, | ||
| }); | ||
| return pauseUntilSignal(run, { resumeStep: "collect" }); // pass the handle, not the signal name | ||
@@ -353,3 +365,5 @@ }, | ||
| if (result.executionEnvironment?.type === "blaxel_sandbox") { | ||
| const sandbox = ctx.sapiom.sandboxes.attach(result.executionEnvironment.id); | ||
| const sandbox = ctx.sapiom.sandboxes.attach( | ||
| result.executionEnvironment.id, | ||
| ); | ||
| // … push from sandbox, read files, etc. | ||
@@ -383,3 +397,3 @@ } | ||
| resumeStep: "finalize", | ||
| correlationId: ctx.executionId, // makes the awaited signal unique to this execution | ||
| correlationId: ctx.executionId, // makes the awaited signal unique to this execution | ||
| }); | ||
@@ -389,4 +403,4 @@ ``` | ||
| Under `run_local`, a dispatch pause auto-resumes with the stub result; a manual gate | ||
| auto-resumes with `{}` unless stubbed — type the resumed step's input with optional fields | ||
| accordingly. | ||
| auto-resumes with `{}`. There is no manual-signal payload override in the local runner, so | ||
| type the resumed step's input with optional fields accordingly. | ||
@@ -411,8 +425,14 @@ ## Determinism | ||
| "launch": { | ||
| "models.coding.run": { "status": "completed", "summary": "done", "result": null, "error": null, "executionEnvironment": null } | ||
| "models.coding.run": { | ||
| "status": "completed", | ||
| "summary": "done", | ||
| "result": null, | ||
| "error": null, | ||
| "executionEnvironment": null, | ||
| }, | ||
| }, | ||
| "check": { | ||
| "repositories.list": [{ "slug": "my-repo", "cloneUrl": "https://..." }] | ||
| } | ||
| } | ||
| "repositories.list": [{ "slug": "my-repo", "cloneUrl": "https://..." }], | ||
| }, | ||
| }, | ||
| } | ||
@@ -431,7 +451,10 @@ ``` | ||
| means the stub silently didn't apply. | ||
| - **Local retry cap:** the `run_local` tool defaults to `maxAttemptsPerStep: 3`. If a step's | ||
| own retry bound allows ≥3 retries, pass a higher `maxAttemptsPerStep` so the local harness | ||
| doesn't stop the loop before your `fail()` fires. This cap is local-test only — production | ||
| has no engine-level retry cap. | ||
| - **Attempt cap:** local and production execution both allow three attempts per step by | ||
| default, counting the initial attempt. The local tool exposes `maxAttemptsPerStep` for | ||
| targeted testing, but raising it does not change production's default ceiling. | ||
| Only `ctx.sapiom.*` calls are replaced. The definition import and each step body are ordinary | ||
| local code, so direct network requests, filesystem writes, environment reads, and child | ||
| processes still happen. Inspect those effects and any third-party billing before running. | ||
| Write each step the way it should run in production — never weaken logic to shape a local run. | ||
@@ -456,20 +479,20 @@ | ||
| | Symptom | Cause | Fix | | ||
| |---|---|---| | ||
| | `Cannot find module '@sapiom/agent'` | Deps not installed | `npm install` inside the scaffolded dir | | ||
| | Type error: `fail(...)` not assignable | Step missing `canFail: true` | Add `canFail: true` to `defineStep` | | ||
| | Type error: `terminate(...)` not assignable | Step missing `terminal: true` | Add `terminal: true` to `defineStep` | | ||
| | `goto` target rejected by types | Target not in `next[]` | Add the target name to `next` | | ||
| | `check` fails: step missing from graph | `steps` object key doesn't match `name` field | Match the key in `steps: { start }` to `defineStep({ name: "start" })` | | ||
| | `run_local` reports `unusedStubs` | Stub path typo or namespace/handle mix-up | Namespace path for calls (`repositories.list`), singular for handles (`repository.pushFromSandbox`) | | ||
| | Paused step resumes with empty input | Manual gate; `run_local` auto-resumes with `{}` | Type the resumed step's input with optional fields | | ||
| | `sapiom_authenticate` → credential not found at deploy | Authenticated in a different shell | Re-run `sapiom_authenticate`; credential is per-machine in `~/.sapiom/credentials.json` | | ||
| | Symptom | Cause | Fix | | ||
| | ------------------------------------------------------ | ----------------------------------------------- | --------------------------------------------------------------------------------------------------- | | ||
| | `Cannot find module '@sapiom/agent'` | Deps not installed | `npm install` inside the scaffolded dir | | ||
| | Type error: `fail(...)` not assignable | Step missing `canFail: true` | Add `canFail: true` to `defineStep` | | ||
| | Type error: `terminate(...)` not assignable | Step missing `terminal: true` | Add `terminal: true` to `defineStep` | | ||
| | `goto` target rejected by types | Target not in `next[]` | Add the target name to `next` | | ||
| | `check` fails: step missing from graph | `steps` object key doesn't match `name` field | Match the key in `steps: { start }` to `defineStep({ name: "start" })` | | ||
| | `run_local` reports `unusedStubs` | Stub path typo or namespace/handle mix-up | Namespace path for calls (`repositories.list`), singular for handles (`repository.pushFromSandbox`) | | ||
| | Paused step resumes with empty input | Manual gate; `run_local` auto-resumes with `{}` | Type the resumed step's input with optional fields | | ||
| | `sapiom_authenticate` → credential not found at deploy | Authenticated in a different shell | Re-run `sapiom_authenticate`; credential is per-machine in `~/.sapiom/credentials.json` | | ||
| ## References | ||
| | Resource | What it covers | | ||
| |---|---| | ||
| | Resource | What it covers | | ||
| | ---------------------------------------------------------- | ------------------------------------------------------------ | | ||
| | [Authoring guide](https://docs.sapiom.ai/agents/authoring) | Full step model, failure patterns, pause/resume, determinism | | ||
| | [Quickstart](https://docs.sapiom.ai/agents/quick-start) | Scaffold → write → test → deploy walkthrough | | ||
| | [Capabilities](https://docs.sapiom.ai/capabilities) | The full `ctx.sapiom.*` catalog with pricing | | ||
| | `AGENTS.md` in your scaffold | The quick in-project reference | | ||
| | [Quickstart](https://docs.sapiom.ai/agents/quick-start) | Scaffold → write → test → deploy walkthrough | | ||
| | [Capabilities](https://docs.sapiom.ai/capabilities) | The full `ctx.sapiom.*` catalog with pricing | | ||
| | `AGENTS.md` in your scaffold | The quick in-project reference | |
@@ -19,4 +19,5 @@ --- | ||
| paid Sapiom capabilities through the typed `ctx.sapiom.*` client — and returns a directive. | ||
| You test it locally for free, then deploy it to run on Sapiom's cloud: on demand, on a | ||
| schedule, or resumed by signals. All from the terminal; no dashboard required. | ||
| You test it locally without a Sapiom account or capability spend, then deploy it to run on | ||
| Sapiom's cloud: on demand, on a schedule, or resumed by signals. All from the terminal; no | ||
| dashboard required. | ||
@@ -40,10 +41,4 @@ **Load this skill before scaffolding — it drives the whole lifecycle from zero.** Inside a | ||
| ### 1. Authenticate (required before deploy/run) | ||
| ### 1. Scaffold | ||
| Run `sapiom_authenticate` — it opens a browser login and caches an API key in | ||
| `~/.sapiom/credentials.json`. Confirm with `sapiom_status`. This makes your coding agent an | ||
| API-key principal; deployed agents inherit that authority to call paid capabilities. | ||
| ### 2. Scaffold | ||
| Call `sapiom_dev_agents_scaffold` with a target directory. The scaffold writes: | ||
@@ -64,19 +59,29 @@ | ||
| ### 3. Write steps → typecheck → check → run_local → deploy | ||
| ### 2. Write steps → typecheck → check → run_local | ||
| | Command | What it does | | ||
| |---|---| | ||
| | `npm run typecheck` | Confirms types compile and every `ctx.sapiom.*` call exists | | ||
| | `sapiom_dev_agents_check` | Bundles `index.ts` + validates the step graph (offline, instant) | | ||
| | `sapiom_dev_agents_run_local` | Runs real step code with all capabilities stubbed — free, no spend | | ||
| | Command | What it does | | ||
| | ----------------------------- | -------------------------------------------------------------------------------------------------------------------- | | ||
| | `npm run typecheck` | Confirms types compile and every `ctx.sapiom.*` call exists | | ||
| | `sapiom_dev_agents_check` | Typechecks, bundles and imports `index.ts`, then validates the manifest and graph; no Sapiom account or service call | | ||
| | `sapiom_dev_agents_run_local` | Runs real step code with `ctx.sapiom.*` calls stubbed — no Sapiom capability spend | | ||
| Then ship: | ||
| `check` imports your definition, and `run_local` executes your real step bodies. Neither | ||
| contacts a Sapiom service, but author-written top-level or step code can still use the local | ||
| filesystem, process, environment, network, and third-party services. | ||
| | Command | What it does | | ||
| |---|---| | ||
| | `sapiom_dev_agents_link` | Registers the agent under your tenant | | ||
| | `sapiom_dev_agents_deploy` | Builds and deploys to Sapiom's cloud | | ||
| | `sapiom_dev_agents_run` | Starts a real (billed) execution | | ||
| | `sapiom_dev_agents_inspect` | Watch an execution's status, steps, and spend | | ||
| ### 3. Authenticate before cloud work | ||
| Run `sapiom_authenticate` — it opens a browser login and caches an API key in | ||
| `~/.sapiom/credentials.json`. Confirm with `sapiom_status`. This makes your coding agent an | ||
| API-key principal; link, deploy, and cloud run require it. | ||
| ### 4. Link → deploy → run → inspect | ||
| | Command | What it does | | ||
| | --------------------------- | --------------------------------------------------- | | ||
| | `sapiom_dev_agents_link` | Registers the agent under your tenant | | ||
| | `sapiom_dev_agents_deploy` | Builds and deploys to Sapiom's cloud | | ||
| | `sapiom_dev_agents_run` | Starts a real (billed) execution | | ||
| | `sapiom_dev_agents_inspect` | Watch status, pinned build, steps, logs, and output | | ||
| ## The Step Model — Hard Rules | ||
@@ -86,9 +91,9 @@ | ||
| | Import | From | | ||
| |---|---| | ||
| | `defineAgent` | `@sapiom/agent` | | ||
| | `defineStep` | `@sapiom/agent` | | ||
| | Import | From | | ||
| | ---------------------------------------------------- | --------------- | | ||
| | `defineAgent` | `@sapiom/agent` | | ||
| | `defineStep` | `@sapiom/agent` | | ||
| | `goto / terminate / fail / retry / pauseUntilSignal` | `@sapiom/agent` | | ||
| | `AgentExecutionContext` | `@sapiom/agent` | | ||
| | `CODING_RESULT_SIGNAL / CodingResultPayload` | `@sapiom/tools` | | ||
| | `AgentExecutionContext` | `@sapiom/agent` | | ||
| | `CODING_RESULT_SIGNAL / CodingResultPayload` | `@sapiom/tools` | | ||
@@ -101,4 +106,4 @@ `@sapiom/agent` is the only authoring package. | ||
| export const agent = defineAgent({ | ||
| name: "my-agent", // string — used for logging and inspect | ||
| entry: "start", // must name a key in steps | ||
| name: "my-agent", // string — used for logging and inspect | ||
| entry: "start", // must name a key in steps | ||
| steps: { start, finish }, | ||
@@ -112,12 +117,12 @@ }); | ||
| | Field | Type | Required | Notes | | ||
| |---|---|---|---| | ||
| | `name` | `string` | yes | Step's id; must match its key in the steps object | | ||
| | `next` | `readonly string[]` | yes | Step names this step may `goto`. Empty array if terminal | | ||
| | `terminal` | `boolean` | no | `true` if this step ends the agent's execution | | ||
| | `canFail` | `boolean` | no | Must be `true` to return `fail()` | | ||
| | `pause` | `{ signal, resumeStep }` | no | Required when returning `pauseUntilSignal(...)` | | ||
| | `inputSchema` | `ZodType` | no | Zod schema validating this step's input. On the **entry** step it is the agent's public API (see [The Entry Input Contract](#the-entry-input-contract--your-agents-public-api)) | | ||
| | `timeoutMs` | `number` | no | Per-step timeout; no automatic retry cap | | ||
| | `run(input, ctx)` | `async function` | yes | Returns a directive | | ||
| | Field | Type | Required | Notes | | ||
| | ----------------- | ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| | `name` | `string` | yes | Step's id; must match its key in the steps object | | ||
| | `next` | `readonly string[]` | yes | Step names this step may `goto`. Empty array if terminal | | ||
| | `terminal` | `boolean` | no | `true` if this step ends the agent's execution | | ||
| | `canFail` | `boolean` | no | Must be `true` to return `fail()` | | ||
| | `pause` | `{ signal, resumeStep }` | no | Required when returning `pauseUntilSignal(...)` | | ||
| | `inputSchema` | `ZodType` | no | Zod schema validating this step's input. On the **entry** step it is the agent's public API (see [The Entry Input Contract](#the-entry-input-contract--your-agents-public-api)) | | ||
| | `timeoutMs` | `number` | no | Per-attempt step timeout; the engine separately caps attempts (three by default) | | ||
| | `run(input, ctx)` | `async function` | yes | Returns a directive | | ||
@@ -129,9 +134,9 @@ Import Zod via the `zod/v4` subpath — `import { z } from "zod/v4"` — to match the SDK's | ||
| | Directive | Function | Constraint | | ||
| |---|---|---| | ||
| | `goto(target, output?)` | Advance to another step | `target` must be in `next[]` | | ||
| | `terminate(output?, opts?)` | End the execution successfully | Step must have `terminal: true` | | ||
| | `fail(reason?, opts?)` | End the execution as failed | Step must have `canFail: true` | | ||
| | `retry(opts?)` | Re-run this step | Bound with `ctx.attempts` — no automatic cap | | ||
| | `pauseUntilSignal(handle, opts?)` | Suspend until a signal fires | Step must declare `pause: { signal, resumeStep }` | | ||
| | Directive | Function | Constraint | | ||
| | --------------------------------- | ------------------------------ | --------------------------------------------------------- | | ||
| | `goto(target, output?)` | Advance to another step | `target` must be in `next[]` | | ||
| | `terminate(output?, opts?)` | End the execution successfully | Step must have `terminal: true` | | ||
| | `fail(reason?, opts?)` | End the execution as failed | Step must have `canFail: true` | | ||
| | `retry(opts?)` | Re-run this step | Explicit retry, capped at three total attempts by default | | ||
| | `pauseUntilSignal(handle, opts?)` | Suspend until a signal fires | Step must declare `pause: { signal, resumeStep }` | | ||
@@ -182,3 +187,3 @@ TypeScript enforces these constraints at compile time — a `terminate` in a non-terminal step, | ||
| Declare it on the entry step even when the agent looks input-free: an entry step with **no** | ||
| `inputSchema` tells the platform the agent takes *no* input, so the dashboard renders an | ||
| `inputSchema` tells the platform the agent takes _no_ input, so the dashboard renders an | ||
| empty Run form and callers have nothing to fill in (and `check` warns). Give every field a | ||
@@ -247,14 +252,14 @@ `.default()` so a zero-input run — the dashboard "Run" button with an empty form — still | ||
| | Field | Type | Notes | | ||
| |---|---|---| | ||
| | `ctx.executionId` | `string` | Unique id for this execution | | ||
| | `ctx.agentName` | `string` | The agent's `name` | | ||
| | `ctx.input` | `unknown` | The execution's entry input — same value the entry step's `run` arg receives. Use `ctx.shared` to carry it forward; don't rely on `ctx.input` downstream. | | ||
| | `ctx.shared` | `TypedContextStore<TShared>` | Cross-step key/value store | | ||
| | `ctx.history` | `readonly StepExecutionRecord[]` | Previous steps' records | | ||
| | `ctx.attempts` | `number` | How many times this step has run (0-indexed) | | ||
| | `ctx.logger` | `StepLogger` | `info / warn / error / debug(msg, meta?)` | | ||
| | `ctx.sapiom` | `Sapiom` | The typed capability client — the `Sapiom` interface from `@sapiom/tools`, installed in your `node_modules` (see "Capabilities" below) | | ||
| | `ctx.organizationId` | `string \| null` | Tenant org | | ||
| | `ctx.tenantId` | `string \| null` | Tenant id | | ||
| | Field | Type | Notes | | ||
| | -------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| | `ctx.executionId` | `string` | Unique id for this execution | | ||
| | `ctx.agentName` | `string` | The agent's `name` | | ||
| | `ctx.input` | `unknown` | The execution's entry input — same value the entry step's `run` arg receives. Use `ctx.shared` to carry it forward; don't rely on `ctx.input` downstream. | | ||
| | `ctx.shared` | `TypedContextStore<TShared>` | Cross-step key/value store | | ||
| | `ctx.history` | `readonly StepExecutionRecord[]` | Previous steps' records | | ||
| | `ctx.attempts` | `number` | How many times this step has run (0-indexed) | | ||
| | `ctx.logger` | `StepLogger` | `info / warn / error / debug(msg, meta?)` | | ||
| | `ctx.sapiom` | `Sapiom` | The typed capability client — the `Sapiom` interface from `@sapiom/tools`, installed in your `node_modules` (see "Capabilities" below) | | ||
| | `ctx.organizationId` | `string \| null` | Tenant org | | ||
| | `ctx.tenantId` | `string \| null` | Tenant id | | ||
@@ -305,4 +310,4 @@ ## Capabilities from Steps | ||
| `timeoutMs` on a step caps how long its `run` may take. There is no engine-level retry cap — | ||
| you own the bound. | ||
| `timeoutMs` caps one attempt of a step's `run`. The engine allows three attempts per step by | ||
| default, counting the initial attempt; keep author-controlled retry logic inside that ceiling. | ||
@@ -318,3 +323,7 @@ ## Pause & Resume (Long-Running Dispatched Steps) | ||
| import { | ||
| defineAgent, defineStep, goto, pauseUntilSignal, terminate, | ||
| defineAgent, | ||
| defineStep, | ||
| goto, | ||
| pauseUntilSignal, | ||
| terminate, | ||
| type AgentExecutionContext, | ||
@@ -335,4 +344,7 @@ } from "@sapiom/agent"; | ||
| const repo = await ctx.sapiom.repositories.create("my-repo"); | ||
| ctx.shared.set("repoSlug", repo.slug); // stash before pausing | ||
| const run = await ctx.sapiom.models.coding.launch({ task: input.task, gitRepository: repo }); | ||
| ctx.shared.set("repoSlug", repo.slug); // stash before pausing | ||
| const run = await ctx.sapiom.models.coding.launch({ | ||
| task: input.task, | ||
| gitRepository: repo, | ||
| }); | ||
| return pauseUntilSignal(run, { resumeStep: "collect" }); // pass the handle, not the signal name | ||
@@ -353,3 +365,5 @@ }, | ||
| if (result.executionEnvironment?.type === "blaxel_sandbox") { | ||
| const sandbox = ctx.sapiom.sandboxes.attach(result.executionEnvironment.id); | ||
| const sandbox = ctx.sapiom.sandboxes.attach( | ||
| result.executionEnvironment.id, | ||
| ); | ||
| // … push from sandbox, read files, etc. | ||
@@ -383,3 +397,3 @@ } | ||
| resumeStep: "finalize", | ||
| correlationId: ctx.executionId, // makes the awaited signal unique to this execution | ||
| correlationId: ctx.executionId, // makes the awaited signal unique to this execution | ||
| }); | ||
@@ -389,4 +403,4 @@ ``` | ||
| Under `run_local`, a dispatch pause auto-resumes with the stub result; a manual gate | ||
| auto-resumes with `{}` unless stubbed — type the resumed step's input with optional fields | ||
| accordingly. | ||
| auto-resumes with `{}`. There is no manual-signal payload override in the local runner, so | ||
| type the resumed step's input with optional fields accordingly. | ||
@@ -411,8 +425,14 @@ ## Determinism | ||
| "launch": { | ||
| "models.coding.run": { "status": "completed", "summary": "done", "result": null, "error": null, "executionEnvironment": null } | ||
| "models.coding.run": { | ||
| "status": "completed", | ||
| "summary": "done", | ||
| "result": null, | ||
| "error": null, | ||
| "executionEnvironment": null, | ||
| }, | ||
| }, | ||
| "check": { | ||
| "repositories.list": [{ "slug": "my-repo", "cloneUrl": "https://..." }] | ||
| } | ||
| } | ||
| "repositories.list": [{ "slug": "my-repo", "cloneUrl": "https://..." }], | ||
| }, | ||
| }, | ||
| } | ||
@@ -431,7 +451,10 @@ ``` | ||
| means the stub silently didn't apply. | ||
| - **Local retry cap:** the `run_local` tool defaults to `maxAttemptsPerStep: 3`. If a step's | ||
| own retry bound allows ≥3 retries, pass a higher `maxAttemptsPerStep` so the local harness | ||
| doesn't stop the loop before your `fail()` fires. This cap is local-test only — production | ||
| has no engine-level retry cap. | ||
| - **Attempt cap:** local and production execution both allow three attempts per step by | ||
| default, counting the initial attempt. The local tool exposes `maxAttemptsPerStep` for | ||
| targeted testing, but raising it does not change production's default ceiling. | ||
| Only `ctx.sapiom.*` calls are replaced. The definition import and each step body are ordinary | ||
| local code, so direct network requests, filesystem writes, environment reads, and child | ||
| processes still happen. Inspect those effects and any third-party billing before running. | ||
| Write each step the way it should run in production — never weaken logic to shape a local run. | ||
@@ -456,20 +479,20 @@ | ||
| | Symptom | Cause | Fix | | ||
| |---|---|---| | ||
| | `Cannot find module '@sapiom/agent'` | Deps not installed | `npm install` inside the scaffolded dir | | ||
| | Type error: `fail(...)` not assignable | Step missing `canFail: true` | Add `canFail: true` to `defineStep` | | ||
| | Type error: `terminate(...)` not assignable | Step missing `terminal: true` | Add `terminal: true` to `defineStep` | | ||
| | `goto` target rejected by types | Target not in `next[]` | Add the target name to `next` | | ||
| | `check` fails: step missing from graph | `steps` object key doesn't match `name` field | Match the key in `steps: { start }` to `defineStep({ name: "start" })` | | ||
| | `run_local` reports `unusedStubs` | Stub path typo or namespace/handle mix-up | Namespace path for calls (`repositories.list`), singular for handles (`repository.pushFromSandbox`) | | ||
| | Paused step resumes with empty input | Manual gate; `run_local` auto-resumes with `{}` | Type the resumed step's input with optional fields | | ||
| | `sapiom_authenticate` → credential not found at deploy | Authenticated in a different shell | Re-run `sapiom_authenticate`; credential is per-machine in `~/.sapiom/credentials.json` | | ||
| | Symptom | Cause | Fix | | ||
| | ------------------------------------------------------ | ----------------------------------------------- | --------------------------------------------------------------------------------------------------- | | ||
| | `Cannot find module '@sapiom/agent'` | Deps not installed | `npm install` inside the scaffolded dir | | ||
| | Type error: `fail(...)` not assignable | Step missing `canFail: true` | Add `canFail: true` to `defineStep` | | ||
| | Type error: `terminate(...)` not assignable | Step missing `terminal: true` | Add `terminal: true` to `defineStep` | | ||
| | `goto` target rejected by types | Target not in `next[]` | Add the target name to `next` | | ||
| | `check` fails: step missing from graph | `steps` object key doesn't match `name` field | Match the key in `steps: { start }` to `defineStep({ name: "start" })` | | ||
| | `run_local` reports `unusedStubs` | Stub path typo or namespace/handle mix-up | Namespace path for calls (`repositories.list`), singular for handles (`repository.pushFromSandbox`) | | ||
| | Paused step resumes with empty input | Manual gate; `run_local` auto-resumes with `{}` | Type the resumed step's input with optional fields | | ||
| | `sapiom_authenticate` → credential not found at deploy | Authenticated in a different shell | Re-run `sapiom_authenticate`; credential is per-machine in `~/.sapiom/credentials.json` | | ||
| ## References | ||
| | Resource | What it covers | | ||
| |---|---| | ||
| | Resource | What it covers | | ||
| | ---------------------------------------------------------- | ------------------------------------------------------------ | | ||
| | [Authoring guide](https://docs.sapiom.ai/agents/authoring) | Full step model, failure patterns, pause/resume, determinism | | ||
| | [Quickstart](https://docs.sapiom.ai/agents/quick-start) | Scaffold → write → test → deploy walkthrough | | ||
| | [Capabilities](https://docs.sapiom.ai/capabilities) | The full `ctx.sapiom.*` catalog with pricing | | ||
| | `AGENTS.md` in your scaffold | The quick in-project reference | | ||
| | [Quickstart](https://docs.sapiom.ai/agents/quick-start) | Scaffold → write → test → deploy walkthrough | | ||
| | [Capabilities](https://docs.sapiom.ai/capabilities) | The full `ctx.sapiom.*` catalog with pricing | | ||
| | `AGENTS.md` in your scaffold | The quick in-project reference | |
@@ -28,3 +28,4 @@ # Working in this agent project | ||
| }), | ||
| async run(input, ctx) { // input: { repo: string; window: "day" | "week" | "month" } | ||
| async run(input, ctx) { | ||
| // input: { repo: string; window: "day" | "week" | "month" } | ||
| return terminate({ scanned: input.repo }); | ||
@@ -43,5 +44,7 @@ }, | ||
| - **check** — typecheck + bundle + manifest + step-graph validation. The full local pre-flight before deploy. | ||
| - **run_local** — runs your **real** step code locally against **stub capabilities**: every `ctx.sapiom.*` call (namespace calls *and* handle methods like `repo.pushFromSandbox`) returns a built-in default, so an agent run completes end-to-end with zero setup. Returns a per-step trace. | ||
| - **run_local** — runs your **real** step code locally against **stub capabilities**: every `ctx.sapiom.*` call (namespace calls _and_ handle methods like `repo.pushFromSandbox`) returns a built-in default, so an agent run completes end-to-end with zero setup. Returns a per-step trace. | ||
| - **deploy** — ship it. | ||
| Only `ctx.sapiom.*` calls are replaced. Definition imports and step bodies are ordinary local code, so direct network requests, filesystem writes, environment reads, and child processes still happen during `check` or `run_local`. | ||
| > Write each step the way it should run in production. `run_local` adapts to your code (stub capabilities), not the other way around — never weaken or drop real logic to shape a local run. | ||
@@ -58,3 +61,3 @@ | ||
| - Capability paths are namespace methods (`repositories.list`, `repositories.create`, `models.coding.run`) or handle methods, which use the **singular** handle type (`repository.pushFromSandbox`, `sandbox.exec`) — not the plural namespace. | ||
| - `<response>` is returned **verbatim** — it is the value that call would return, so match its real shape. `repositories.list` takes the array `list()` returns: `[{ "slug": "...", "cloneUrl": "..." }]` (each element a repository — *not* `[[ … ]]`). `repositories.create`/`get`/`attach` take a single `{ "slug", "cloneUrl" }`. | ||
| - `<response>` is returned **verbatim** — it is the value that call would return, so match its real shape. `repositories.list` takes the array `list()` returns: `[{ "slug": "...", "cloneUrl": "..." }]` (each element a repository — _not_ `[[ … ]]`). `repositories.create`/`get`/`attach` take a single `{ "slug", "cloneUrl" }`. | ||
| - `run_local` reports **`unusedStubs`** (a key that matched no call — usually a typo or the plural/singular mistake) and **`stubWarnings`** (a key matched but the value was the wrong shape). A green run with either non-empty means a stub silently didn't take effect — check them. | ||
@@ -67,4 +70,7 @@ | ||
| ```ts | ||
| const run = await ctx.sapiom.models.coding.launch({ task, gitRepository: repo }); // returns a handle, not a result | ||
| return pauseUntilSignal(run, { resumeStep: "finalize" }); // suspend on the run's result signal | ||
| const run = await ctx.sapiom.models.coding.launch({ | ||
| task, | ||
| gitRepository: repo, | ||
| }); // returns a handle, not a result | ||
| return pauseUntilSignal(run, { resumeStep: "finalize" }); // suspend on the run's result signal | ||
| ``` | ||
@@ -74,3 +80,3 @@ | ||
| - That payload crossed a wire boundary, so it carries **no live handles** — to act on the run's sandbox, re-attach one from **`executionEnvironment`** with `ctx.sapiom.sandboxes.attach(result.executionEnvironment.id)` (`executionEnvironment` is `null` when the run provisioned none, e.g. a launch failure). Anything else the resumed step needs, stash in `ctx.shared` before pausing. | ||
| - **To stub the resume payload** (e.g. to exercise the failure branch), override `models.coding.run` *in the launching step* — that one value is both the `run()` result and the payload the paused step resumes with. `models.coding.launch` is accepted there too. | ||
| - **To stub the resume payload** (e.g. to exercise the failure branch), override `models.coding.run` _in the launching step_ — that one value is both the `run()` result and the payload the paused step resumes with. `models.coding.launch` is accepted there too. | ||
@@ -77,0 +83,0 @@ ## Determinism |
@@ -12,3 +12,3 @@ # __PROJECT_NAME__ | ||
| - **kickoff** calls `models.coding.launch(...)` (which returns a handle, *not* a | ||
| - **kickoff** calls `models.coding.launch(...)` (which returns a handle, _not_ a | ||
| result) and returns `pauseUntilSignal(handle, { resumeStep: "finalize" })`. The | ||
@@ -22,7 +22,18 @@ agent run suspends — a long run holds no worker. | ||
| State that must survive the pause goes in `ctx.shared`. To test the **failure** | ||
| branch locally, stub a failed result under the *launching* step in | ||
| branch locally, stub a failed result under the _launching_ step in | ||
| `.sapiom-dev/stubs.json` — that value is also the resume payload: | ||
| ```jsonc | ||
| { "version": 1, "steps": { "kickoff": { "models.coding.launch": { "status": "failed", "result": { "success": false }, "error": { "stage": "run", "message": "…" } } } } } | ||
| { | ||
| "version": 1, | ||
| "steps": { | ||
| "kickoff": { | ||
| "models.coding.launch": { | ||
| "status": "failed", | ||
| "result": { "success": false }, | ||
| "error": { "stage": "run", "message": "…" }, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| ``` | ||
@@ -39,4 +50,4 @@ | ||
| ```ts | ||
| const box = await ctx.sapiom.sandboxes.create({ name: 'demo' }); | ||
| const repo = await ctx.sapiom.repositories.create('my-repo'); | ||
| const box = await ctx.sapiom.sandboxes.create({ name: "demo" }); | ||
| const repo = await ctx.sapiom.repositories.create("my-repo"); | ||
| ``` | ||
@@ -50,4 +61,4 @@ | ||
| - **check** — validate locally (bundle, manifest, step graph). Offline. | ||
| - **run_local** — execute the steps locally against stubs (no real capability calls), iterating until it completes. | ||
| - **check** — typecheck, bundle and import the definition, then validate its manifest and step graph. No Sapiom account or service call is required. | ||
| - **run_local** — execute the real steps locally with `ctx.sapiom.*` calls resolved from stubs, iterating until completion without Sapiom capability spend. Ordinary code in the project can still make its own network or machine changes. | ||
| - **deploy** — build and ship. | ||
@@ -54,0 +65,0 @@ |
@@ -19,4 +19,5 @@ --- | ||
| paid Sapiom capabilities through the typed `ctx.sapiom.*` client — and returns a directive. | ||
| You test it locally for free, then deploy it to run on Sapiom's cloud: on demand, on a | ||
| schedule, or resumed by signals. All from the terminal; no dashboard required. | ||
| You test it locally without a Sapiom account or capability spend, then deploy it to run on | ||
| Sapiom's cloud: on demand, on a schedule, or resumed by signals. All from the terminal; no | ||
| dashboard required. | ||
@@ -40,10 +41,4 @@ **Load this skill before scaffolding — it drives the whole lifecycle from zero.** Inside a | ||
| ### 1. Authenticate (required before deploy/run) | ||
| ### 1. Scaffold | ||
| Run `sapiom_authenticate` — it opens a browser login and caches an API key in | ||
| `~/.sapiom/credentials.json`. Confirm with `sapiom_status`. This makes your coding agent an | ||
| API-key principal; deployed agents inherit that authority to call paid capabilities. | ||
| ### 2. Scaffold | ||
| Call `sapiom_dev_agents_scaffold` with a target directory. The scaffold writes: | ||
@@ -64,19 +59,29 @@ | ||
| ### 3. Write steps → typecheck → check → run_local → deploy | ||
| ### 2. Write steps → typecheck → check → run_local | ||
| | Command | What it does | | ||
| |---|---| | ||
| | `npm run typecheck` | Confirms types compile and every `ctx.sapiom.*` call exists | | ||
| | `sapiom_dev_agents_check` | Bundles `index.ts` + validates the step graph (offline, instant) | | ||
| | `sapiom_dev_agents_run_local` | Runs real step code with all capabilities stubbed — free, no spend | | ||
| | Command | What it does | | ||
| | ----------------------------- | -------------------------------------------------------------------------------------------------------------------- | | ||
| | `npm run typecheck` | Confirms types compile and every `ctx.sapiom.*` call exists | | ||
| | `sapiom_dev_agents_check` | Typechecks, bundles and imports `index.ts`, then validates the manifest and graph; no Sapiom account or service call | | ||
| | `sapiom_dev_agents_run_local` | Runs real step code with `ctx.sapiom.*` calls stubbed — no Sapiom capability spend | | ||
| Then ship: | ||
| `check` imports your definition, and `run_local` executes your real step bodies. Neither | ||
| contacts a Sapiom service, but author-written top-level or step code can still use the local | ||
| filesystem, process, environment, network, and third-party services. | ||
| | Command | What it does | | ||
| |---|---| | ||
| | `sapiom_dev_agents_link` | Registers the agent under your tenant | | ||
| | `sapiom_dev_agents_deploy` | Builds and deploys to Sapiom's cloud | | ||
| | `sapiom_dev_agents_run` | Starts a real (billed) execution | | ||
| | `sapiom_dev_agents_inspect` | Watch an execution's status, steps, and spend | | ||
| ### 3. Authenticate before cloud work | ||
| Run `sapiom_authenticate` — it opens a browser login and caches an API key in | ||
| `~/.sapiom/credentials.json`. Confirm with `sapiom_status`. This makes your coding agent an | ||
| API-key principal; link, deploy, and cloud run require it. | ||
| ### 4. Link → deploy → run → inspect | ||
| | Command | What it does | | ||
| | --------------------------- | --------------------------------------------------- | | ||
| | `sapiom_dev_agents_link` | Registers the agent under your tenant | | ||
| | `sapiom_dev_agents_deploy` | Builds and deploys to Sapiom's cloud | | ||
| | `sapiom_dev_agents_run` | Starts a real (billed) execution | | ||
| | `sapiom_dev_agents_inspect` | Watch status, pinned build, steps, logs, and output | | ||
| ## The Step Model — Hard Rules | ||
@@ -86,9 +91,9 @@ | ||
| | Import | From | | ||
| |---|---| | ||
| | `defineAgent` | `@sapiom/agent` | | ||
| | `defineStep` | `@sapiom/agent` | | ||
| | Import | From | | ||
| | ---------------------------------------------------- | --------------- | | ||
| | `defineAgent` | `@sapiom/agent` | | ||
| | `defineStep` | `@sapiom/agent` | | ||
| | `goto / terminate / fail / retry / pauseUntilSignal` | `@sapiom/agent` | | ||
| | `AgentExecutionContext` | `@sapiom/agent` | | ||
| | `CODING_RESULT_SIGNAL / CodingResultPayload` | `@sapiom/tools` | | ||
| | `AgentExecutionContext` | `@sapiom/agent` | | ||
| | `CODING_RESULT_SIGNAL / CodingResultPayload` | `@sapiom/tools` | | ||
@@ -101,4 +106,4 @@ `@sapiom/agent` is the only authoring package. | ||
| export const agent = defineAgent({ | ||
| name: "my-agent", // string — used for logging and inspect | ||
| entry: "start", // must name a key in steps | ||
| name: "my-agent", // string — used for logging and inspect | ||
| entry: "start", // must name a key in steps | ||
| steps: { start, finish }, | ||
@@ -112,12 +117,12 @@ }); | ||
| | Field | Type | Required | Notes | | ||
| |---|---|---|---| | ||
| | `name` | `string` | yes | Step's id; must match its key in the steps object | | ||
| | `next` | `readonly string[]` | yes | Step names this step may `goto`. Empty array if terminal | | ||
| | `terminal` | `boolean` | no | `true` if this step ends the agent's execution | | ||
| | `canFail` | `boolean` | no | Must be `true` to return `fail()` | | ||
| | `pause` | `{ signal, resumeStep }` | no | Required when returning `pauseUntilSignal(...)` | | ||
| | `inputSchema` | `ZodType` | no | Zod schema validating this step's input. On the **entry** step it is the agent's public API (see [The Entry Input Contract](#the-entry-input-contract--your-agents-public-api)) | | ||
| | `timeoutMs` | `number` | no | Per-step timeout; no automatic retry cap | | ||
| | `run(input, ctx)` | `async function` | yes | Returns a directive | | ||
| | Field | Type | Required | Notes | | ||
| | ----------------- | ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| | `name` | `string` | yes | Step's id; must match its key in the steps object | | ||
| | `next` | `readonly string[]` | yes | Step names this step may `goto`. Empty array if terminal | | ||
| | `terminal` | `boolean` | no | `true` if this step ends the agent's execution | | ||
| | `canFail` | `boolean` | no | Must be `true` to return `fail()` | | ||
| | `pause` | `{ signal, resumeStep }` | no | Required when returning `pauseUntilSignal(...)` | | ||
| | `inputSchema` | `ZodType` | no | Zod schema validating this step's input. On the **entry** step it is the agent's public API (see [The Entry Input Contract](#the-entry-input-contract--your-agents-public-api)) | | ||
| | `timeoutMs` | `number` | no | Per-attempt step timeout; the engine separately caps attempts (three by default) | | ||
| | `run(input, ctx)` | `async function` | yes | Returns a directive | | ||
@@ -129,9 +134,9 @@ Import Zod via the `zod/v4` subpath — `import { z } from "zod/v4"` — to match the SDK's | ||
| | Directive | Function | Constraint | | ||
| |---|---|---| | ||
| | `goto(target, output?)` | Advance to another step | `target` must be in `next[]` | | ||
| | `terminate(output?, opts?)` | End the execution successfully | Step must have `terminal: true` | | ||
| | `fail(reason?, opts?)` | End the execution as failed | Step must have `canFail: true` | | ||
| | `retry(opts?)` | Re-run this step | Bound with `ctx.attempts` — no automatic cap | | ||
| | `pauseUntilSignal(handle, opts?)` | Suspend until a signal fires | Step must declare `pause: { signal, resumeStep }` | | ||
| | Directive | Function | Constraint | | ||
| | --------------------------------- | ------------------------------ | --------------------------------------------------------- | | ||
| | `goto(target, output?)` | Advance to another step | `target` must be in `next[]` | | ||
| | `terminate(output?, opts?)` | End the execution successfully | Step must have `terminal: true` | | ||
| | `fail(reason?, opts?)` | End the execution as failed | Step must have `canFail: true` | | ||
| | `retry(opts?)` | Re-run this step | Explicit retry, capped at three total attempts by default | | ||
| | `pauseUntilSignal(handle, opts?)` | Suspend until a signal fires | Step must declare `pause: { signal, resumeStep }` | | ||
@@ -182,3 +187,3 @@ TypeScript enforces these constraints at compile time — a `terminate` in a non-terminal step, | ||
| Declare it on the entry step even when the agent looks input-free: an entry step with **no** | ||
| `inputSchema` tells the platform the agent takes *no* input, so the dashboard renders an | ||
| `inputSchema` tells the platform the agent takes _no_ input, so the dashboard renders an | ||
| empty Run form and callers have nothing to fill in (and `check` warns). Give every field a | ||
@@ -247,14 +252,14 @@ `.default()` so a zero-input run — the dashboard "Run" button with an empty form — still | ||
| | Field | Type | Notes | | ||
| |---|---|---| | ||
| | `ctx.executionId` | `string` | Unique id for this execution | | ||
| | `ctx.agentName` | `string` | The agent's `name` | | ||
| | `ctx.input` | `unknown` | The execution's entry input — same value the entry step's `run` arg receives. Use `ctx.shared` to carry it forward; don't rely on `ctx.input` downstream. | | ||
| | `ctx.shared` | `TypedContextStore<TShared>` | Cross-step key/value store | | ||
| | `ctx.history` | `readonly StepExecutionRecord[]` | Previous steps' records | | ||
| | `ctx.attempts` | `number` | How many times this step has run (0-indexed) | | ||
| | `ctx.logger` | `StepLogger` | `info / warn / error / debug(msg, meta?)` | | ||
| | `ctx.sapiom` | `Sapiom` | The typed capability client — the `Sapiom` interface from `@sapiom/tools`, installed in your `node_modules` (see "Capabilities" below) | | ||
| | `ctx.organizationId` | `string \| null` | Tenant org | | ||
| | `ctx.tenantId` | `string \| null` | Tenant id | | ||
| | Field | Type | Notes | | ||
| | -------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| | `ctx.executionId` | `string` | Unique id for this execution | | ||
| | `ctx.agentName` | `string` | The agent's `name` | | ||
| | `ctx.input` | `unknown` | The execution's entry input — same value the entry step's `run` arg receives. Use `ctx.shared` to carry it forward; don't rely on `ctx.input` downstream. | | ||
| | `ctx.shared` | `TypedContextStore<TShared>` | Cross-step key/value store | | ||
| | `ctx.history` | `readonly StepExecutionRecord[]` | Previous steps' records | | ||
| | `ctx.attempts` | `number` | How many times this step has run (0-indexed) | | ||
| | `ctx.logger` | `StepLogger` | `info / warn / error / debug(msg, meta?)` | | ||
| | `ctx.sapiom` | `Sapiom` | The typed capability client — the `Sapiom` interface from `@sapiom/tools`, installed in your `node_modules` (see "Capabilities" below) | | ||
| | `ctx.organizationId` | `string \| null` | Tenant org | | ||
| | `ctx.tenantId` | `string \| null` | Tenant id | | ||
@@ -305,4 +310,4 @@ ## Capabilities from Steps | ||
| `timeoutMs` on a step caps how long its `run` may take. There is no engine-level retry cap — | ||
| you own the bound. | ||
| `timeoutMs` caps one attempt of a step's `run`. The engine allows three attempts per step by | ||
| default, counting the initial attempt; keep author-controlled retry logic inside that ceiling. | ||
@@ -318,3 +323,7 @@ ## Pause & Resume (Long-Running Dispatched Steps) | ||
| import { | ||
| defineAgent, defineStep, goto, pauseUntilSignal, terminate, | ||
| defineAgent, | ||
| defineStep, | ||
| goto, | ||
| pauseUntilSignal, | ||
| terminate, | ||
| type AgentExecutionContext, | ||
@@ -335,4 +344,7 @@ } from "@sapiom/agent"; | ||
| const repo = await ctx.sapiom.repositories.create("my-repo"); | ||
| ctx.shared.set("repoSlug", repo.slug); // stash before pausing | ||
| const run = await ctx.sapiom.models.coding.launch({ task: input.task, gitRepository: repo }); | ||
| ctx.shared.set("repoSlug", repo.slug); // stash before pausing | ||
| const run = await ctx.sapiom.models.coding.launch({ | ||
| task: input.task, | ||
| gitRepository: repo, | ||
| }); | ||
| return pauseUntilSignal(run, { resumeStep: "collect" }); // pass the handle, not the signal name | ||
@@ -353,3 +365,5 @@ }, | ||
| if (result.executionEnvironment?.type === "blaxel_sandbox") { | ||
| const sandbox = ctx.sapiom.sandboxes.attach(result.executionEnvironment.id); | ||
| const sandbox = ctx.sapiom.sandboxes.attach( | ||
| result.executionEnvironment.id, | ||
| ); | ||
| // … push from sandbox, read files, etc. | ||
@@ -383,3 +397,3 @@ } | ||
| resumeStep: "finalize", | ||
| correlationId: ctx.executionId, // makes the awaited signal unique to this execution | ||
| correlationId: ctx.executionId, // makes the awaited signal unique to this execution | ||
| }); | ||
@@ -389,4 +403,4 @@ ``` | ||
| Under `run_local`, a dispatch pause auto-resumes with the stub result; a manual gate | ||
| auto-resumes with `{}` unless stubbed — type the resumed step's input with optional fields | ||
| accordingly. | ||
| auto-resumes with `{}`. There is no manual-signal payload override in the local runner, so | ||
| type the resumed step's input with optional fields accordingly. | ||
@@ -411,8 +425,14 @@ ## Determinism | ||
| "launch": { | ||
| "models.coding.run": { "status": "completed", "summary": "done", "result": null, "error": null, "executionEnvironment": null } | ||
| "models.coding.run": { | ||
| "status": "completed", | ||
| "summary": "done", | ||
| "result": null, | ||
| "error": null, | ||
| "executionEnvironment": null, | ||
| }, | ||
| }, | ||
| "check": { | ||
| "repositories.list": [{ "slug": "my-repo", "cloneUrl": "https://..." }] | ||
| } | ||
| } | ||
| "repositories.list": [{ "slug": "my-repo", "cloneUrl": "https://..." }], | ||
| }, | ||
| }, | ||
| } | ||
@@ -431,7 +451,10 @@ ``` | ||
| means the stub silently didn't apply. | ||
| - **Local retry cap:** the `run_local` tool defaults to `maxAttemptsPerStep: 3`. If a step's | ||
| own retry bound allows ≥3 retries, pass a higher `maxAttemptsPerStep` so the local harness | ||
| doesn't stop the loop before your `fail()` fires. This cap is local-test only — production | ||
| has no engine-level retry cap. | ||
| - **Attempt cap:** local and production execution both allow three attempts per step by | ||
| default, counting the initial attempt. The local tool exposes `maxAttemptsPerStep` for | ||
| targeted testing, but raising it does not change production's default ceiling. | ||
| Only `ctx.sapiom.*` calls are replaced. The definition import and each step body are ordinary | ||
| local code, so direct network requests, filesystem writes, environment reads, and child | ||
| processes still happen. Inspect those effects and any third-party billing before running. | ||
| Write each step the way it should run in production — never weaken logic to shape a local run. | ||
@@ -456,20 +479,20 @@ | ||
| | Symptom | Cause | Fix | | ||
| |---|---|---| | ||
| | `Cannot find module '@sapiom/agent'` | Deps not installed | `npm install` inside the scaffolded dir | | ||
| | Type error: `fail(...)` not assignable | Step missing `canFail: true` | Add `canFail: true` to `defineStep` | | ||
| | Type error: `terminate(...)` not assignable | Step missing `terminal: true` | Add `terminal: true` to `defineStep` | | ||
| | `goto` target rejected by types | Target not in `next[]` | Add the target name to `next` | | ||
| | `check` fails: step missing from graph | `steps` object key doesn't match `name` field | Match the key in `steps: { start }` to `defineStep({ name: "start" })` | | ||
| | `run_local` reports `unusedStubs` | Stub path typo or namespace/handle mix-up | Namespace path for calls (`repositories.list`), singular for handles (`repository.pushFromSandbox`) | | ||
| | Paused step resumes with empty input | Manual gate; `run_local` auto-resumes with `{}` | Type the resumed step's input with optional fields | | ||
| | `sapiom_authenticate` → credential not found at deploy | Authenticated in a different shell | Re-run `sapiom_authenticate`; credential is per-machine in `~/.sapiom/credentials.json` | | ||
| | Symptom | Cause | Fix | | ||
| | ------------------------------------------------------ | ----------------------------------------------- | --------------------------------------------------------------------------------------------------- | | ||
| | `Cannot find module '@sapiom/agent'` | Deps not installed | `npm install` inside the scaffolded dir | | ||
| | Type error: `fail(...)` not assignable | Step missing `canFail: true` | Add `canFail: true` to `defineStep` | | ||
| | Type error: `terminate(...)` not assignable | Step missing `terminal: true` | Add `terminal: true` to `defineStep` | | ||
| | `goto` target rejected by types | Target not in `next[]` | Add the target name to `next` | | ||
| | `check` fails: step missing from graph | `steps` object key doesn't match `name` field | Match the key in `steps: { start }` to `defineStep({ name: "start" })` | | ||
| | `run_local` reports `unusedStubs` | Stub path typo or namespace/handle mix-up | Namespace path for calls (`repositories.list`), singular for handles (`repository.pushFromSandbox`) | | ||
| | Paused step resumes with empty input | Manual gate; `run_local` auto-resumes with `{}` | Type the resumed step's input with optional fields | | ||
| | `sapiom_authenticate` → credential not found at deploy | Authenticated in a different shell | Re-run `sapiom_authenticate`; credential is per-machine in `~/.sapiom/credentials.json` | | ||
| ## References | ||
| | Resource | What it covers | | ||
| |---|---| | ||
| | Resource | What it covers | | ||
| | ---------------------------------------------------------- | ------------------------------------------------------------ | | ||
| | [Authoring guide](https://docs.sapiom.ai/agents/authoring) | Full step model, failure patterns, pause/resume, determinism | | ||
| | [Quickstart](https://docs.sapiom.ai/agents/quick-start) | Scaffold → write → test → deploy walkthrough | | ||
| | [Capabilities](https://docs.sapiom.ai/capabilities) | The full `ctx.sapiom.*` catalog with pricing | | ||
| | `AGENTS.md` in your scaffold | The quick in-project reference | | ||
| | [Quickstart](https://docs.sapiom.ai/agents/quick-start) | Scaffold → write → test → deploy walkthrough | | ||
| | [Capabilities](https://docs.sapiom.ai/capabilities) | The full `ctx.sapiom.*` catalog with pricing | | ||
| | `AGENTS.md` in your scaffold | The quick in-project reference | |
@@ -28,3 +28,4 @@ # Working in this agent project | ||
| }), | ||
| async run(input, ctx) { // input: { repo: string; window: "day" | "week" | "month" } | ||
| async run(input, ctx) { | ||
| // input: { repo: string; window: "day" | "week" | "month" } | ||
| return terminate({ scanned: input.repo }); | ||
@@ -43,5 +44,7 @@ }, | ||
| - **check** — typecheck + bundle + manifest + step-graph validation. The full local pre-flight before deploy. | ||
| - **run_local** — runs your **real** step code locally against **stub capabilities**: every `ctx.sapiom.*` call (namespace calls *and* handle methods like `repo.pushFromSandbox`) returns a built-in default, so an agent run completes end-to-end with zero setup. Returns a per-step trace. | ||
| - **run_local** — runs your **real** step code locally against **stub capabilities**: every `ctx.sapiom.*` call (namespace calls _and_ handle methods like `repo.pushFromSandbox`) returns a built-in default, so an agent run completes end-to-end with zero setup. Returns a per-step trace. | ||
| - **deploy** — ship it. | ||
| Only `ctx.sapiom.*` calls are replaced. Definition imports and step bodies are ordinary local code, so direct network requests, filesystem writes, environment reads, and child processes still happen during `check` or `run_local`. | ||
| > Write each step the way it should run in production. `run_local` adapts to your code (stub capabilities), not the other way around — never weaken or drop real logic to shape a local run. | ||
@@ -58,3 +61,3 @@ | ||
| - Capability paths are namespace methods (`repositories.list`, `repositories.create`, `models.coding.run`) or handle methods, which use the **singular** handle type (`repository.pushFromSandbox`, `sandbox.exec`) — not the plural namespace. | ||
| - `<response>` is returned **verbatim** — it is the value that call would return, so match its real shape. `repositories.list` takes the array `list()` returns: `[{ "slug": "...", "cloneUrl": "..." }]` (each element a repository — *not* `[[ … ]]`). `repositories.create`/`get`/`attach` take a single `{ "slug", "cloneUrl" }`. | ||
| - `<response>` is returned **verbatim** — it is the value that call would return, so match its real shape. `repositories.list` takes the array `list()` returns: `[{ "slug": "...", "cloneUrl": "..." }]` (each element a repository — _not_ `[[ … ]]`). `repositories.create`/`get`/`attach` take a single `{ "slug", "cloneUrl" }`. | ||
| - `run_local` reports **`unusedStubs`** (a key that matched no call — usually a typo or the plural/singular mistake) and **`stubWarnings`** (a key matched but the value was the wrong shape). A green run with either non-empty means a stub silently didn't take effect — check them. | ||
@@ -67,4 +70,7 @@ | ||
| ```ts | ||
| const run = await ctx.sapiom.models.coding.launch({ task, gitRepository: repo }); // returns a handle, not a result | ||
| return pauseUntilSignal(run, { resumeStep: "finalize" }); // suspend on the run's result signal | ||
| const run = await ctx.sapiom.models.coding.launch({ | ||
| task, | ||
| gitRepository: repo, | ||
| }); // returns a handle, not a result | ||
| return pauseUntilSignal(run, { resumeStep: "finalize" }); // suspend on the run's result signal | ||
| ``` | ||
@@ -74,3 +80,3 @@ | ||
| - That payload crossed a wire boundary, so it carries **no live handles** — to act on the run's sandbox, re-attach one from **`executionEnvironment`** with `ctx.sapiom.sandboxes.attach(result.executionEnvironment.id)` (`executionEnvironment` is `null` when the run provisioned none, e.g. a launch failure). Anything else the resumed step needs, stash in `ctx.shared` before pausing. | ||
| - **To stub the resume payload** (e.g. to exercise the failure branch), override `models.coding.run` *in the launching step* — that one value is both the `run()` result and the payload the paused step resumes with. `models.coding.launch` is accepted there too. | ||
| - **To stub the resume payload** (e.g. to exercise the failure branch), override `models.coding.run` _in the launching step_ — that one value is both the `run()` result and the payload the paused step resumes with. `models.coding.launch` is accepted there too. | ||
@@ -77,0 +83,0 @@ ## Determinism |
@@ -14,4 +14,4 @@ # __PROJECT_NAME__ | ||
| ```ts | ||
| const box = await ctx.sapiom.sandboxes.create({ name: 'demo' }); | ||
| const repo = await ctx.sapiom.repositories.create('my-repo'); | ||
| const box = await ctx.sapiom.sandboxes.create({ name: "demo" }); | ||
| const repo = await ctx.sapiom.repositories.create("my-repo"); | ||
| ``` | ||
@@ -25,4 +25,4 @@ | ||
| - **check** — validate locally (bundle, manifest, step graph). Offline. | ||
| - **run_local** — execute the steps locally against stubs (no real capability calls), iterating until it completes. | ||
| - **check** — typecheck, bundle and import the definition, then validate its manifest and step graph. No Sapiom account or service call is required. | ||
| - **run_local** — execute the real steps locally with `ctx.sapiom.*` calls resolved from stubs, iterating until completion without Sapiom capability spend. Ordinary code in the project can still make its own network or machine changes. | ||
| - **deploy** — build and ship. | ||
@@ -29,0 +29,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
497567
6.1%5761
4.33%10
11.11%Updated