agent-contracts-runtime
Version:
Runtime bridge for executing agent-contracts workflows on Agent SDKs
244 lines • 8.92 kB
JavaScript
// src/adapters/claude-agent-sdk.ts
function buildClaudeHooks(guardrails) {
const preToolUseHook = {
hooks: [
(input) => {
const toolInput = input.tool_input;
const toolName = input.tool_name;
if (toolName === "Bash" || toolName === "Shell") {
const command = toolInput?.command ?? toolInput?.input;
if (command) {
const result = guardrails.beforeShellExecution({ command });
if (result.permission === "deny") {
return {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: result.user_message ?? result.agent_message
};
}
if (result.additionalContext) {
return { hookEventName: "PreToolUse", additionalContext: result.additionalContext };
}
}
}
const filePath = toolInput?.file_path ?? toolInput?.path;
if (filePath) {
const pathResult = guardrails.preToolUse({
tool_name: toolName,
tool_input: { file_path: filePath }
});
if (pathResult.permission === "deny") {
return {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: pathResult.user_message ?? pathResult.agent_message
};
}
if (pathResult.additionalContext) {
return { hookEventName: "PreToolUse", additionalContext: pathResult.additionalContext };
}
}
return { hookEventName: "PreToolUse" };
}
]
};
return { PreToolUse: [preToolUseHook] };
}
var ClaudeAgentSdkAdapter = class _ClaudeAgentSdkAdapter {
cwd;
model;
tools;
permissionMode;
maxTurns;
guardrailHooks;
cacheConfig;
lastSessionId = null;
lastMemoryRef = null;
queryFn = null;
constructor(config = {}) {
this.cwd = config.cwd ?? process.cwd();
this.model = config.model;
this.tools = config.tools;
this.permissionMode = config.permissionMode ?? "bypassPermissions";
this.maxTurns = config.maxTurns;
this.guardrailHooks = config.guardrailHooks;
this.cacheConfig = config.cacheConfig ?? { enabled: true };
}
// -------------------------------------------------------------------------
// Internal helpers
// -------------------------------------------------------------------------
async resolveQueryFn() {
if (this.queryFn) return this.queryFn;
const sdk = await import("@anthropic-ai/claude-agent-sdk");
this.queryFn = sdk.query;
return this.queryFn;
}
buildOptions(readonly, resume, systemPrompt, agents, allowDynamicWorkflow) {
const opts = {
cwd: this.cwd,
permissionMode: this.permissionMode,
allowDangerouslySkipPermissions: this.permissionMode === "bypassPermissions",
// SDK isolation: do not load ambient .claude/ settings, skills, or agents
// from disk. The runtime injects everything in-code, so disk discovery
// would break component isolation and reproducibility.
settingSources: []
};
if (this.model) opts.model = this.model;
if (this.maxTurns) opts.maxTurns = this.maxTurns;
let toolList;
if (this.tools) {
toolList = Array.isArray(this.tools) ? [...this.tools] : this.tools;
} else {
toolList = readonly ? ["Read", "Glob", "Grep"] : ["Read", "Glob", "Grep", "Edit", "Write", "Bash"];
}
if (agents && agents.length > 0 && Array.isArray(toolList)) {
for (const t of ["Task", "Agent"]) {
if (!toolList.includes(t)) toolList.push(t);
}
}
if (allowDynamicWorkflow && Array.isArray(toolList)) {
if (!toolList.includes("Workflow")) toolList.push("Workflow");
}
opts.tools = toolList;
if (resume) opts.resume = resume;
if (systemPrompt && this.cacheConfig.enabled !== false) {
opts.systemPrompt = systemPrompt;
}
if (agents && agents.length > 0) {
const record = {};
for (const a of agents) {
record[a.name] = {
description: a.description,
prompt: a.prompt,
...a.tools ? { tools: a.tools } : {},
...a.model ? { model: a.model } : {}
};
}
opts.agents = record;
}
if (this.guardrailHooks) {
opts.hooks = buildClaudeHooks(this.guardrailHooks);
}
return opts;
}
async runQuery(prompt, options, onProgress) {
const queryFn = await this.resolveQueryFn();
const stream = queryFn({ prompt, options });
let resultText = "";
let sessionId = "";
for await (const message of stream) {
if (message.session_id) sessionId = message.session_id;
if (message.type === "result") {
if (message.subtype === "success") {
resultText = message.result;
} else if (message.subtype === "error") {
throw new Error(`Claude Agent SDK error: ${message.error}`);
}
} else if (onProgress) {
emitProgressFromSdkMessage(message, onProgress, sessionId);
}
}
return { text: resultText, sessionId };
}
// -------------------------------------------------------------------------
// SdkAdapter interface
// -------------------------------------------------------------------------
async send(prompt, options) {
this.lastSessionId = null;
this.lastMemoryRef = null;
const split = options.splitPrompt;
const systemPrompt = split?.system;
const userPrompt = split ? split.user : prompt;
const opts = this.buildOptions(options.readonly, void 0, systemPrompt, options.agents, options.allowDynamicWorkflow);
const { text, sessionId } = await this.runQuery(userPrompt, opts, options.onProgress);
this.lastSessionId = sessionId;
this.lastMemoryRef = {
id: sessionId,
provider: "claude-agent-sdk",
compat: "claude-agent-sdk@0.2",
created_at: (/* @__PURE__ */ new Date()).toISOString()
};
return text;
}
async followUp(message) {
if (!this.lastSessionId) {
throw new Error("followUp() called before send() \u2014 no active session");
}
const opts = this.buildOptions(false, this.lastSessionId);
const { text, sessionId } = await this.runQuery(message, opts);
this.lastSessionId = sessionId;
return text;
}
async sendExecution(request) {
this.lastSessionId = null;
this.lastMemoryRef = null;
const resume = request.memoryRef?.id;
const split = request.splitPrompt;
const systemPrompt = split?.system;
const userPrompt = split ? split.user : request.prompt;
const agents = request.options.agents ?? request.agents;
const opts = this.buildOptions(request.options.readonly, resume, systemPrompt, agents, request.options.allowDynamicWorkflow);
const { text, sessionId } = await this.runQuery(userPrompt, opts, request.options.onProgress);
this.lastSessionId = sessionId;
this.lastMemoryRef = {
id: sessionId,
provider: "claude-agent-sdk",
compat: "claude-agent-sdk@0.2",
created_at: (/* @__PURE__ */ new Date()).toISOString(),
parent_run_id: request.memoryRef?.id
};
return text;
}
getLastMemoryRef() {
return this.lastMemoryRef;
}
isCompatible(compat) {
return compat.startsWith("claude-agent-sdk@");
}
// -------------------------------------------------------------------------
// Test support
// -------------------------------------------------------------------------
/**
* Inject a custom query function for testing.
* Bypasses the dynamic import of @anthropic-ai/claude-agent-sdk.
*/
static withQueryFn(queryFn, config) {
const adapter = new _ClaudeAgentSdkAdapter(config);
adapter.queryFn = queryFn;
return adapter;
}
};
function emitProgressFromSdkMessage(message, onProgress, sessionId) {
const sid = message.session_id ?? sessionId;
switch (message.type) {
case "assistant": {
const content = message.message?.content;
if (!content) break;
for (const block of content) {
if (block.type === "tool_use" && block.name) {
const input = block.input && typeof block.input === "object" ? block.input : void 0;
onProgress({ type: "tool_use", tool_name: block.name, input, session_id: sid });
} else if (block.type === "text" && block.text) {
onProgress({ type: "text", message: block.text, session_id: sid });
}
}
break;
}
case "tool_progress":
if (message.tool_name) {
onProgress({ type: "tool_use", tool_name: message.tool_name, session_id: sid });
}
break;
case "tool_use_summary":
if (message.summary) {
onProgress({ type: "status", message: message.summary, session_id: sid });
}
break;
default:
break;
}
}
export {
ClaudeAgentSdkAdapter
};
//# sourceMappingURL=claude-agent-sdk.js.map