mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-08-28 02:44:59 +00:00
refactor: extract provider adapter registry (#1230)
This commit is contained in:
parent
7cc8f253c7
commit
93ea9577d2
6 changed files with 761 additions and 463 deletions
35
AGENTS.md
35
AGENTS.md
|
|
@ -492,23 +492,24 @@ it.
|
|||
|
||||
## File locations quick-reference
|
||||
|
||||
| What | Where |
|
||||
| ---------------- | ------------------------------------- |
|
||||
| API routes | `server/src/routes/` |
|
||||
| Services | `server/src/services/` |
|
||||
| Zod schemas | `server/src/schemas/` |
|
||||
| Storage | `server/src/storage/` |
|
||||
| Server utilities | `server/src/utils/` |
|
||||
| React components | `web/src/components/` |
|
||||
| Zustand stores | `web/src/stores/` |
|
||||
| CLI commands | `cli/src/commands/` |
|
||||
| Shared types | `shared/src/` |
|
||||
| MCP server | `mcp/src/` |
|
||||
| Prompt registry | `prompt-registry/` |
|
||||
| SOPs | `docs/SOP-*.md` |
|
||||
| Agent registry | `.veritas-kanban/agent-registry.json` |
|
||||
| Agent run logs | `.veritas-kanban/logs/` |
|
||||
| Telemetry events | `.veritas-kanban/telemetry/` |
|
||||
| What | Where |
|
||||
| ----------------- | -------------------------------------------------------- |
|
||||
| API routes | `server/src/routes/` |
|
||||
| Services | `server/src/services/` |
|
||||
| Zod schemas | `server/src/schemas/` |
|
||||
| Storage | `server/src/storage/` |
|
||||
| Server utilities | `server/src/utils/` |
|
||||
| Provider adapters | `server/src/services/agent-provider-adapter-registry.ts` |
|
||||
| React components | `web/src/components/` |
|
||||
| Zustand stores | `web/src/stores/` |
|
||||
| CLI commands | `cli/src/commands/` |
|
||||
| Shared types | `shared/src/` |
|
||||
| MCP server | `mcp/src/` |
|
||||
| Prompt registry | `prompt-registry/` |
|
||||
| SOPs | `docs/SOP-*.md` |
|
||||
| Agent registry | `.veritas-kanban/agent-registry.json` |
|
||||
| Agent run logs | `.veritas-kanban/logs/` |
|
||||
| Telemetry events | `.veritas-kanban/telemetry/` |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1073,6 +1073,22 @@ execution-tree identity to the durable attempt. Missing or inconsistent
|
|||
evidence fails before the adapter is called, so an adapter cannot widen
|
||||
capacity or substitute an external hidden queue.
|
||||
|
||||
### Provider adapter lifecycle ownership
|
||||
|
||||
`server/src/services/agent-provider-adapter-registry.ts` is the executable
|
||||
provider-selection authority. Its `resolve(provider, surface)` interface owns
|
||||
the exact adapter identity, task-envelope renderer, runtime probe, run-event
|
||||
mapper, start dispatch, and stop behavior for every executable provider.
|
||||
Unknown or non-executable providers fail before this seam; there is no implicit
|
||||
OpenClaw fallback.
|
||||
|
||||
`ClawdbotAgentService` remains the shared run orchestrator. It supplies
|
||||
admission, supervisor, sandbox, budget, journal, and completion effects to the
|
||||
registry host without duplicating provider selection. Full attempt mutations
|
||||
cross `AttemptLifecycleCoordinator`, which verifies active-attempt ownership,
|
||||
optimistic revisions, history maintenance, and terminal completion binding.
|
||||
Provider adapters never write attempt state directly.
|
||||
|
||||
Active leases renew while the verified run is live; completion, interruption,
|
||||
cancellation, or launch failure releases the reservation idempotently.
|
||||
Workflow retry and fallback attempts release the prior step reservation before
|
||||
|
|
|
|||
|
|
@ -80,7 +80,17 @@ POST /api/github/codex/delegate
|
|||
|
||||
## Architecture Direction
|
||||
|
||||
v4.3 uses an explicit provider adapter contract inside the agent service. `codex` agents resolve to the local Codex CLI runner, `codex-sdk` agents resolve to the SDK session runner, `codex-cloud` uses GitHub-native delegation, and existing agents keep the OpenClaw request-file behavior.
|
||||
Executable task providers resolve through the dedicated
|
||||
`AgentProviderAdapterRegistry`. The registry owns exact provider selection,
|
||||
task-envelope rendering, runtime probing, run-event mapping, start dispatch,
|
||||
and stop semantics. `ClawdbotAgentService` supplies shared admission,
|
||||
supervision, journaling, budget, and completion effects without selecting an
|
||||
implicit fallback adapter.
|
||||
|
||||
`codex` agents resolve to the local Codex CLI runner, `codex-sdk` agents resolve
|
||||
to the SDK session runner, and `codex-cloud` uses GitHub-native delegation.
|
||||
OpenClaw task dispatch uses the gateway `sessions_spawn` path and persists the
|
||||
returned session identity on the active attempt.
|
||||
|
||||
Expected long-term provider capabilities:
|
||||
|
||||
|
|
@ -93,7 +103,7 @@ Expected long-term provider capabilities:
|
|||
- optional `review`
|
||||
- optional `cloudDelegate`
|
||||
|
||||
The provider abstraction should support:
|
||||
The provider adapter interface supports:
|
||||
|
||||
- OpenClaw compatibility through an OpenClaw provider adapter.
|
||||
- Codex CLI through a local process provider.
|
||||
|
|
|
|||
177
server/src/__tests__/agent-provider-adapter-registry.test.ts
Normal file
177
server/src/__tests__/agent-provider-adapter-registry.test.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import { EventEmitter } from 'node:events';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { EXECUTABLE_AGENT_PROVIDERS, type ExecutableAgentProvider } from '@veritas-kanban/shared';
|
||||
import {
|
||||
AgentProviderAdapterRegistry,
|
||||
type AgentProviderAdapterHost,
|
||||
type AgentProviderStartContext,
|
||||
} from '../services/agent-provider-adapter-registry.js';
|
||||
import { providerRuntimeManifestFixture } from './fixtures/provider-runtime-manifest.js';
|
||||
|
||||
const RENDERER_NAMES: Record<ExecutableAgentProvider, string> = {
|
||||
'codex-cli': 'renderCodexCliTaskEnvelope',
|
||||
'codex-sdk': 'renderCodexSdkTaskEnvelope',
|
||||
'codex-app-server': 'renderCodexAppServerTaskEnvelope',
|
||||
'acp-stdio': 'renderAcpStdioTaskEnvelope',
|
||||
'claude-code': 'renderClaudeCodeTaskEnvelope',
|
||||
'hermes-cli': 'renderHermesTaskEnvelope',
|
||||
openclaw: 'renderOpenClawTaskEnvelope',
|
||||
};
|
||||
|
||||
function createHost(): AgentProviderAdapterHost {
|
||||
return {
|
||||
probe: vi.fn(async (provider) =>
|
||||
providerRuntimeManifestFixture({ provider, adapter: provider })
|
||||
),
|
||||
probeAcp: vi.fn(async () =>
|
||||
providerRuntimeManifestFixture({ provider: 'acp-stdio', adapter: 'acp-stdio' })
|
||||
),
|
||||
assertTransport: vi.fn(),
|
||||
getPending: vi.fn(),
|
||||
startCodexCli: vi.fn(async () => undefined),
|
||||
startCodexSdk: vi.fn(async () => undefined),
|
||||
handleCodexSdkError: vi.fn(async () => undefined),
|
||||
startCodexAppServer: vi.fn(async () => undefined),
|
||||
startAcpStdio: vi.fn(async () => undefined),
|
||||
startClaudeCode: vi.fn(async () => undefined),
|
||||
startHermesCli: vi.fn(async () => undefined),
|
||||
startOpenClaw: vi.fn(async () => undefined),
|
||||
warn: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
function startContext(provider: ExecutableAgentProvider): AgentProviderStartContext {
|
||||
return {
|
||||
task: { id: 'task_provider_registry' },
|
||||
transport: {
|
||||
schemaVersion: 'provider-task-envelope-transport/v1',
|
||||
provider,
|
||||
taskEnvelopeDigest: 'task-envelope-digest',
|
||||
callbackPosture: provider === 'openclaw' ? 'veritas-http' : 'harness-owned',
|
||||
completionNormalization: 'harness',
|
||||
content: 'Run the task.',
|
||||
},
|
||||
logPath: '/tmp/provider-registry.log',
|
||||
attemptId: 'attempt_provider_registry',
|
||||
startedAt: '2026-08-24T06:00:00.000Z',
|
||||
emitter: new EventEmitter(),
|
||||
attempt: { id: 'attempt_provider_registry', status: 'running', agent: 'codex' },
|
||||
runLaunchManifest: { digest: 'run-launch-digest' },
|
||||
conversation: {
|
||||
schemaVersion: 'conversation-lifecycle/v1',
|
||||
mode: 'fresh',
|
||||
intent: 'fresh',
|
||||
state: 'active',
|
||||
contextWindow: { posture: 'unknown', measuredAt: '2026-08-24T06:00:00.000Z' },
|
||||
createdAt: '2026-08-24T06:00:00.000Z',
|
||||
updatedAt: '2026-08-24T06:00:00.000Z',
|
||||
},
|
||||
admission: {
|
||||
schemaVersion: 'provider-admission-evidence/v1',
|
||||
source: 'direct',
|
||||
outcome: 'admitted',
|
||||
reservationId: 'reservation_provider_registry',
|
||||
executionTree: {
|
||||
rootObjectiveId: 'objective_provider_registry',
|
||||
nodeId: 'node_provider_registry',
|
||||
depth: 0,
|
||||
edge: 'root',
|
||||
},
|
||||
},
|
||||
} as AgentProviderStartContext;
|
||||
}
|
||||
|
||||
describe('AgentProviderAdapterRegistry', () => {
|
||||
it('resolves every executable provider without an implicit fallback', async () => {
|
||||
const host = createHost();
|
||||
const registry = new AgentProviderAdapterRegistry(host);
|
||||
|
||||
for (const provider of EXECUTABLE_AGENT_PROVIDERS) {
|
||||
const adapter = registry.resolve(provider);
|
||||
|
||||
expect(adapter.id).toBe(provider);
|
||||
expect(adapter.renderTaskEnvelope.name).toBe(RENDERER_NAMES[provider]);
|
||||
expect(adapter.runEventMapper.mapEvent).toEqual(expect.any(Function));
|
||||
await expect(
|
||||
adapter.probe({
|
||||
health: {
|
||||
type: provider,
|
||||
name: provider,
|
||||
enabled: true,
|
||||
configured: true,
|
||||
command: provider,
|
||||
executableFound: true,
|
||||
authenticated: true,
|
||||
healthy: true,
|
||||
checkedAt: '2026-08-24T06:00:00.000Z',
|
||||
},
|
||||
})
|
||||
).resolves.toMatchObject({ provider, adapter: provider });
|
||||
}
|
||||
|
||||
expect(host.probeAcp).toHaveBeenCalledOnce();
|
||||
expect(host.probe).toHaveBeenCalledTimes(EXECUTABLE_AGENT_PROVIDERS.length - 1);
|
||||
});
|
||||
|
||||
it('dispatches starts through the exact provider operation', async () => {
|
||||
const host = createHost();
|
||||
const registry = new AgentProviderAdapterRegistry(host);
|
||||
const startOperations: Record<ExecutableAgentProvider, ReturnType<typeof vi.fn>> = {
|
||||
'codex-cli': host.startCodexCli,
|
||||
'codex-sdk': host.startCodexSdk,
|
||||
'codex-app-server': host.startCodexAppServer,
|
||||
'acp-stdio': host.startAcpStdio,
|
||||
'claude-code': host.startClaudeCode,
|
||||
'hermes-cli': host.startHermesCli,
|
||||
openclaw: host.startOpenClaw,
|
||||
};
|
||||
|
||||
for (const provider of EXECUTABLE_AGENT_PROVIDERS) {
|
||||
await registry.resolve(provider).start(startContext(provider));
|
||||
expect(startOperations[provider]).toHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
expect(host.assertTransport).toHaveBeenCalledTimes(EXECUTABLE_AGENT_PROVIDERS.length);
|
||||
});
|
||||
|
||||
it('keeps stop behavior behind the adapter seam', async () => {
|
||||
const host = createHost();
|
||||
const registry = new AgentProviderAdapterRegistry(host);
|
||||
const abortController = new AbortController();
|
||||
const cancel = vi.fn(async () => undefined);
|
||||
const close = vi.fn(async () => undefined);
|
||||
|
||||
await registry.resolve('codex-sdk').stop({
|
||||
taskId: 'task_provider_registry',
|
||||
pending: {
|
||||
taskId: 'task_provider_registry',
|
||||
attemptId: 'attempt_provider_registry',
|
||||
abortController,
|
||||
},
|
||||
});
|
||||
await registry.resolve('acp-stdio').stop({
|
||||
taskId: 'task_provider_registry',
|
||||
pending: {
|
||||
taskId: 'task_provider_registry',
|
||||
attemptId: 'attempt_provider_registry',
|
||||
acpControl: { cancel, close },
|
||||
},
|
||||
});
|
||||
await registry.resolve('openclaw').stop({
|
||||
taskId: 'task_provider_registry',
|
||||
pending: {
|
||||
taskId: 'task_provider_registry',
|
||||
attemptId: 'attempt_provider_registry',
|
||||
openclawSessionKey: 'session_provider_registry',
|
||||
},
|
||||
});
|
||||
|
||||
expect(abortController.signal.aborted).toBe(true);
|
||||
expect(cancel).toHaveBeenCalledOnce();
|
||||
expect(close).toHaveBeenCalledOnce();
|
||||
expect(host.warn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ sessionKey: 'session_provider_registry' }),
|
||||
expect.stringContaining('OpenClaw stop requested')
|
||||
);
|
||||
});
|
||||
});
|
||||
272
server/src/services/agent-provider-adapter-registry.ts
Normal file
272
server/src/services/agent-provider-adapter-registry.ts
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
import type { ChildProcessWithoutNullStreams } from 'node:child_process';
|
||||
import type { EventEmitter } from 'node:events';
|
||||
import type {
|
||||
AdmissionLaunchSource,
|
||||
AgentConfig,
|
||||
ExecutableAgentProvider,
|
||||
ExecutionTreeIdentity,
|
||||
ProviderRuntimeManifest,
|
||||
RunLaunchManifest,
|
||||
SandboxPolicyDryRunResult,
|
||||
Task,
|
||||
TaskAttempt,
|
||||
ConversationLifecycleRecord,
|
||||
} from '@veritas-kanban/shared';
|
||||
import type { AcpStdioControl } from './acp-stdio-adapter.js';
|
||||
import type { AgentProviderProbeContext } from './provider-runtime-resolution.js';
|
||||
import {
|
||||
getProviderRuntimeAdapterDefinition,
|
||||
type ProviderRuntimeAdapterDefinition,
|
||||
type ProviderRuntimeSurface,
|
||||
} from './provider-runtime-adapter-registry.js';
|
||||
import {
|
||||
getProviderRunEventMapper,
|
||||
type ProviderRunEventMapper,
|
||||
} from './provider-run-event-mappers.js';
|
||||
import {
|
||||
renderAcpStdioTaskEnvelope,
|
||||
renderClaudeCodeTaskEnvelope,
|
||||
renderCodexAppServerTaskEnvelope,
|
||||
renderCodexCliTaskEnvelope,
|
||||
renderCodexSdkTaskEnvelope,
|
||||
renderHermesTaskEnvelope,
|
||||
renderOpenClawTaskEnvelope,
|
||||
type ProviderTaskEnvelopeRenderInput,
|
||||
type ProviderTaskEnvelopeTransport,
|
||||
} from './provider-task-envelope-renderer.js';
|
||||
|
||||
export interface AgentProviderAdmissionEvidence {
|
||||
schemaVersion: 'provider-admission-evidence/v1';
|
||||
source: AdmissionLaunchSource;
|
||||
outcome: 'admitted' | 'queued-dispatch';
|
||||
reservationId: string;
|
||||
queueEntryId?: string;
|
||||
executionTree: ExecutionTreeIdentity;
|
||||
}
|
||||
|
||||
export interface AgentProviderStartContext {
|
||||
task: Task;
|
||||
agentConfig?: AgentConfig;
|
||||
transport: ProviderTaskEnvelopeTransport;
|
||||
logPath: string;
|
||||
attemptId: string;
|
||||
startedAt: string;
|
||||
emitter: EventEmitter;
|
||||
attempt: TaskAttempt;
|
||||
sandboxPolicy?: SandboxPolicyDryRunResult;
|
||||
runLaunchManifest: RunLaunchManifest;
|
||||
conversation: ConversationLifecycleRecord;
|
||||
admission: AgentProviderAdmissionEvidence;
|
||||
}
|
||||
|
||||
export interface ProviderAdapterPendingRun {
|
||||
taskId: string;
|
||||
attemptId: string;
|
||||
process?: ChildProcessWithoutNullStreams;
|
||||
abortController?: AbortController;
|
||||
codexAppServerControl?: {
|
||||
interrupt(): Promise<void>;
|
||||
close(): void;
|
||||
};
|
||||
acpControl?: Pick<AcpStdioControl, 'cancel' | 'close'>;
|
||||
openclawSessionKey?: string;
|
||||
}
|
||||
|
||||
export interface AgentProviderStopContext {
|
||||
taskId: string;
|
||||
pending: ProviderAdapterPendingRun;
|
||||
}
|
||||
|
||||
export interface AgentProviderAdapter {
|
||||
id: ExecutableAgentProvider;
|
||||
label: string;
|
||||
renderTaskEnvelope(input: ProviderTaskEnvelopeRenderInput): ProviderTaskEnvelopeTransport;
|
||||
probe(context: AgentProviderProbeContext): Promise<ProviderRuntimeManifest>;
|
||||
runEventMapper: ProviderRunEventMapper;
|
||||
start(context: AgentProviderStartContext): Promise<void> | void;
|
||||
stop(context: AgentProviderStopContext): Promise<void> | void;
|
||||
}
|
||||
|
||||
export interface AgentProviderAdapterHost {
|
||||
probe(
|
||||
provider: ExecutableAgentProvider,
|
||||
context: AgentProviderProbeContext,
|
||||
definition: ProviderRuntimeAdapterDefinition
|
||||
): Promise<ProviderRuntimeManifest>;
|
||||
probeAcp(
|
||||
context: AgentProviderProbeContext,
|
||||
definition: ProviderRuntimeAdapterDefinition
|
||||
): Promise<ProviderRuntimeManifest>;
|
||||
assertTransport(
|
||||
provider: ExecutableAgentProvider,
|
||||
transport: ProviderTaskEnvelopeTransport,
|
||||
manifest: RunLaunchManifest
|
||||
): void;
|
||||
getPending(taskId: string): ProviderAdapterPendingRun | undefined;
|
||||
startCodexCli(context: AgentProviderStartContext): Promise<void>;
|
||||
startCodexSdk(
|
||||
context: AgentProviderStartContext,
|
||||
abortController: AbortController
|
||||
): Promise<void>;
|
||||
handleCodexSdkError(
|
||||
context: AgentProviderStartContext,
|
||||
abortController: AbortController,
|
||||
error: unknown
|
||||
): Promise<void>;
|
||||
startCodexAppServer(context: AgentProviderStartContext): Promise<void>;
|
||||
startAcpStdio(context: AgentProviderStartContext): Promise<void>;
|
||||
startClaudeCode(context: AgentProviderStartContext): Promise<void>;
|
||||
startHermesCli(context: AgentProviderStartContext): Promise<void>;
|
||||
startOpenClaw(context: AgentProviderStartContext): Promise<void>;
|
||||
warn(details: Record<string, unknown>, message: string): void;
|
||||
}
|
||||
|
||||
type TaskEnvelopeRenderer = (
|
||||
input: ProviderTaskEnvelopeRenderInput
|
||||
) => ProviderTaskEnvelopeTransport;
|
||||
|
||||
const TASK_ENVELOPE_RENDERERS: Record<ExecutableAgentProvider, TaskEnvelopeRenderer> = {
|
||||
'codex-cli': renderCodexCliTaskEnvelope,
|
||||
'codex-sdk': renderCodexSdkTaskEnvelope,
|
||||
'codex-app-server': renderCodexAppServerTaskEnvelope,
|
||||
'acp-stdio': renderAcpStdioTaskEnvelope,
|
||||
'claude-code': renderClaudeCodeTaskEnvelope,
|
||||
'hermes-cli': renderHermesTaskEnvelope,
|
||||
openclaw: renderOpenClawTaskEnvelope,
|
||||
};
|
||||
|
||||
/**
|
||||
* Owns executable-provider selection and adapter lifecycle semantics. The host
|
||||
* supplies orchestration effects; callers learn only the resolved adapter
|
||||
* interface and never branch on provider identity themselves.
|
||||
*/
|
||||
export class AgentProviderAdapterRegistry {
|
||||
constructor(private readonly host: AgentProviderAdapterHost) {}
|
||||
|
||||
resolve(
|
||||
provider: ExecutableAgentProvider,
|
||||
surface: ProviderRuntimeSurface = 'task'
|
||||
): AgentProviderAdapter {
|
||||
const definition = getProviderRuntimeAdapterDefinition(provider, surface);
|
||||
return {
|
||||
id: definition.id,
|
||||
label: definition.label,
|
||||
renderTaskEnvelope: TASK_ENVELOPE_RENDERERS[provider],
|
||||
probe: (context) =>
|
||||
provider === 'acp-stdio'
|
||||
? this.host.probeAcp(context, definition)
|
||||
: this.host.probe(provider, context, definition),
|
||||
runEventMapper: getProviderRunEventMapper(provider),
|
||||
start: (context) => {
|
||||
this.host.assertTransport(provider, context.transport, context.runLaunchManifest);
|
||||
return this.start(provider, context);
|
||||
},
|
||||
stop: (context) => this.stop(provider, context),
|
||||
};
|
||||
}
|
||||
|
||||
private start(
|
||||
provider: ExecutableAgentProvider,
|
||||
context: AgentProviderStartContext
|
||||
): Promise<void> | void {
|
||||
switch (provider) {
|
||||
case 'codex-cli':
|
||||
return this.host.startCodexCli(context);
|
||||
case 'codex-sdk': {
|
||||
const abortController = new AbortController();
|
||||
const pending = this.host.getPending(context.task.id);
|
||||
if (pending) pending.abortController = abortController;
|
||||
void this.host
|
||||
.startCodexSdk(context, abortController)
|
||||
.catch((error: unknown) =>
|
||||
this.host.handleCodexSdkError(context, abortController, error)
|
||||
);
|
||||
return;
|
||||
}
|
||||
case 'codex-app-server':
|
||||
return this.host.startCodexAppServer(context);
|
||||
case 'acp-stdio':
|
||||
return this.host.startAcpStdio(context);
|
||||
case 'claude-code':
|
||||
return this.host.startClaudeCode(context);
|
||||
case 'hermes-cli':
|
||||
return this.host.startHermesCli(context);
|
||||
case 'openclaw':
|
||||
return this.host.startOpenClaw(context);
|
||||
}
|
||||
}
|
||||
|
||||
private async stop(
|
||||
provider: ExecutableAgentProvider,
|
||||
{ pending }: AgentProviderStopContext
|
||||
): Promise<void> {
|
||||
switch (provider) {
|
||||
case 'codex-cli':
|
||||
if (pending.process && !pending.process.killed) pending.process.kill('SIGTERM');
|
||||
return;
|
||||
case 'codex-sdk':
|
||||
pending.abortController?.abort();
|
||||
return;
|
||||
case 'codex-app-server': {
|
||||
try {
|
||||
await pending.codexAppServerControl?.interrupt();
|
||||
} catch (error) {
|
||||
this.host.warn(
|
||||
{ err: error, taskId: pending.taskId },
|
||||
'Codex app-server cooperative interrupt failed; closing the supervised process'
|
||||
);
|
||||
}
|
||||
pending.codexAppServerControl?.close();
|
||||
const child = pending.process;
|
||||
if (!child || child.exitCode != null || child.signalCode != null) return;
|
||||
const forcedStop = setTimeout(() => {
|
||||
if (child.exitCode == null && child.signalCode == null) child.kill('SIGKILL');
|
||||
}, 5_000);
|
||||
child.once('close', () => clearTimeout(forcedStop));
|
||||
return;
|
||||
}
|
||||
case 'acp-stdio':
|
||||
pending.abortController?.abort();
|
||||
await pending.acpControl?.cancel().catch(() => undefined);
|
||||
await pending.acpControl?.close().catch(() => undefined);
|
||||
return;
|
||||
case 'claude-code': {
|
||||
const child = pending.process;
|
||||
if (!child || child.exitCode != null || child.signalCode != null) return;
|
||||
child.kill('SIGTERM');
|
||||
const forcedStop = setTimeout(() => {
|
||||
if (child.exitCode == null && child.signalCode == null) {
|
||||
child.kill('SIGKILL');
|
||||
this.host.warn(
|
||||
{ taskId: pending.taskId },
|
||||
'[ClawdbotAgent] Claude Code SIGKILL issued after graceful stop timeout'
|
||||
);
|
||||
}
|
||||
}, 5_000);
|
||||
child.once('close', () => clearTimeout(forcedStop));
|
||||
return;
|
||||
}
|
||||
case 'hermes-cli':
|
||||
if (pending.process && !pending.process.killed) {
|
||||
pending.process.kill('SIGTERM');
|
||||
const forcedStop = setTimeout(() => {
|
||||
if (pending.process && !pending.process.killed) {
|
||||
pending.process.kill('SIGKILL');
|
||||
this.host.warn(
|
||||
{ taskId: pending.taskId },
|
||||
'[ClawdbotAgent] Hermes SIGKILL issued after graceful stop timeout'
|
||||
);
|
||||
}
|
||||
}, 5_000);
|
||||
pending.process.once('close', () => clearTimeout(forcedStop));
|
||||
}
|
||||
return;
|
||||
case 'openclaw':
|
||||
this.host.warn(
|
||||
{ taskId: pending.taskId, sessionKey: pending.openclawSessionKey },
|
||||
'[ClawdbotAgent] OpenClaw stop requested; sub-session will complete via callback'
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -54,17 +54,7 @@ import { buildSafeCodexEnv } from '../utils/codex-env.js';
|
|||
import { getRuntimeDir, getLogsDir } from '../utils/paths.js';
|
||||
import { buildSafeHermesEnv } from '../utils/hermes-env.js';
|
||||
import { HttpOpenClawTaskAdapter } from './openclaw-workflow-adapter.js';
|
||||
import {
|
||||
renderCodexCliTaskEnvelope,
|
||||
renderCodexSdkTaskEnvelope,
|
||||
renderCodexAppServerTaskEnvelope,
|
||||
renderClaudeCodeTaskEnvelope,
|
||||
renderAcpStdioTaskEnvelope,
|
||||
renderHermesTaskEnvelope,
|
||||
renderOpenClawTaskEnvelope,
|
||||
type ProviderTaskEnvelopeRenderInput,
|
||||
type ProviderTaskEnvelopeTransport,
|
||||
} from './provider-task-envelope-renderer.js';
|
||||
import { type ProviderTaskEnvelopeTransport } from './provider-task-envelope-renderer.js';
|
||||
import type { ThreadEvent } from '@openai/codex-sdk';
|
||||
import {
|
||||
evaluateTaskReadiness,
|
||||
|
|
@ -213,11 +203,19 @@ import {
|
|||
type RunEventJournalService,
|
||||
} from './run-event-journal-service.js';
|
||||
import { getRunTerminalService, type RunTerminalService } from './run-terminal-service.js';
|
||||
import { type ProviderMappedRunEvent } from './provider-run-event-mappers.js';
|
||||
import {
|
||||
getProviderRunEventMapper,
|
||||
type ProviderMappedRunEvent,
|
||||
type ProviderRunEventMapper,
|
||||
} from './provider-run-event-mappers.js';
|
||||
AgentProviderAdapterRegistry,
|
||||
type AgentProviderAdapterHost,
|
||||
type AgentProviderAdmissionEvidence,
|
||||
type AgentProviderStartContext,
|
||||
} from './agent-provider-adapter-registry.js';
|
||||
export type {
|
||||
AgentProviderAdapter,
|
||||
AgentProviderAdmissionEvidence,
|
||||
AgentProviderStartContext,
|
||||
AgentProviderStopContext,
|
||||
} from './agent-provider-adapter-registry.js';
|
||||
import {
|
||||
buildClaudeCodeArgs,
|
||||
buildSafeClaudeCodeEnv,
|
||||
|
|
@ -339,45 +337,6 @@ const providerDependencyExecutionOptions = {
|
|||
},
|
||||
} satisfies DependencyCircuitExecutionOptions;
|
||||
|
||||
export interface AgentProviderStartContext {
|
||||
task: Task;
|
||||
agentConfig?: AgentConfig;
|
||||
transport: ProviderTaskEnvelopeTransport;
|
||||
logPath: string;
|
||||
attemptId: string;
|
||||
startedAt: string;
|
||||
emitter: EventEmitter;
|
||||
attempt: TaskAttempt;
|
||||
sandboxPolicy?: SandboxPolicyDryRunResult;
|
||||
runLaunchManifest: RunLaunchManifest;
|
||||
conversation: ConversationLifecycleRecord;
|
||||
admission: AgentProviderAdmissionEvidence;
|
||||
}
|
||||
|
||||
export interface AgentProviderAdmissionEvidence {
|
||||
schemaVersion: 'provider-admission-evidence/v1';
|
||||
source: AdmissionLaunchSource;
|
||||
outcome: 'admitted' | 'queued-dispatch';
|
||||
reservationId: string;
|
||||
queueEntryId?: string;
|
||||
executionTree: ExecutionTreeIdentity;
|
||||
}
|
||||
|
||||
export interface AgentProviderStopContext {
|
||||
taskId: string;
|
||||
pending: PendingAgent;
|
||||
}
|
||||
|
||||
export interface AgentProviderAdapter {
|
||||
id: ExecutableAgentProvider;
|
||||
label: string;
|
||||
renderTaskEnvelope(input: ProviderTaskEnvelopeRenderInput): ProviderTaskEnvelopeTransport;
|
||||
probe(context: AgentProviderProbeContext): Promise<ProviderRuntimeManifest>;
|
||||
runEventMapper: ProviderRunEventMapper;
|
||||
start(context: AgentProviderStartContext): Promise<void> | void;
|
||||
stop(context: AgentProviderStopContext): Promise<void> | void;
|
||||
}
|
||||
|
||||
export interface AgentStatus {
|
||||
taskId: string;
|
||||
attemptId: string;
|
||||
|
|
@ -637,6 +596,7 @@ export class ClawdbotAgentService {
|
|||
private taskEnvelopes: TaskEnvelopeService;
|
||||
private runLaunchManifests: RunLaunchManifestService;
|
||||
private runLaunchCompiler: RunLaunchCompiler;
|
||||
private providerAdapters: AgentProviderAdapterRegistry;
|
||||
private providerCompletions: ProviderCompletionService;
|
||||
private attemptLifecycle: AttemptLifecycleCoordinator;
|
||||
private credentialLeases: CredentialLeaseLifecycle;
|
||||
|
|
@ -767,6 +727,7 @@ export class ClawdbotAgentService {
|
|||
this.workspaceExecutionTrust = workspaceExecutionTrust;
|
||||
this.phaseAuthority = phaseAuthority;
|
||||
this.phaseTransitions = phaseTransitions;
|
||||
this.providerAdapters = this.createProviderAdapterRegistry();
|
||||
this.runLaunchCompiler = new RunLaunchCompiler({
|
||||
runLaunchManifests: this.runLaunchManifests,
|
||||
workspaceFiles: this.workspaceFiles,
|
||||
|
|
@ -1928,7 +1889,7 @@ export class ClawdbotAgentService {
|
|||
: agentConfig;
|
||||
const provider = resolveExecutableAgentProvider(profileAgentConfig, agent);
|
||||
const agentHealth = await this.assertAgentAvailable(agent, profileAgentConfig);
|
||||
const adapter = this.resolveProviderAdapter(provider);
|
||||
const adapter = this.providerAdapters.resolve(provider);
|
||||
const budgetService = getAgentBudgetService();
|
||||
const budgetSources = {
|
||||
workspaceBudget: config.features?.budget?.enabled
|
||||
|
|
@ -2239,7 +2200,7 @@ export class ClawdbotAgentService {
|
|||
: agentConfig;
|
||||
const provider = resolveExecutableAgentProvider(profileAgentConfig, agent);
|
||||
const agentHealth = await this.assertAgentAvailable(agent, profileAgentConfig);
|
||||
const adapter = this.resolveProviderAdapter(provider);
|
||||
const adapter = this.providerAdapters.resolve(provider);
|
||||
const budgetService = getAgentBudgetService();
|
||||
const budgetSources = {
|
||||
workspaceBudget: config.features?.budget?.enabled
|
||||
|
|
@ -3675,7 +3636,7 @@ export class ClawdbotAgentService {
|
|||
attempt.id,
|
||||
undefined,
|
||||
'openclaw',
|
||||
this.resolveProviderAdapter('openclaw').runEventMapper.mapEvent(
|
||||
this.providerAdapters.resolve('openclaw').runEventMapper.mapEvent(
|
||||
'message.completed',
|
||||
{
|
||||
type: 'message.completed',
|
||||
|
|
@ -4015,7 +3976,7 @@ export class ClawdbotAgentService {
|
|||
attemptId,
|
||||
undefined,
|
||||
'openclaw',
|
||||
this.resolveProviderAdapter('openclaw').runEventMapper.mapEvent(
|
||||
this.providerAdapters.resolve('openclaw').runEventMapper.mapEvent(
|
||||
'message.completed',
|
||||
{
|
||||
type: 'message.completed',
|
||||
|
|
@ -4398,7 +4359,7 @@ export class ClawdbotAgentService {
|
|||
if (supervisor.control.kind === 'local-process') {
|
||||
await this.runSupervisor.stopLocalProcess(pending.supervisorId);
|
||||
} else {
|
||||
await this.resolveProviderAdapter(pending.provider).stop({
|
||||
await this.providerAdapters.resolve(pending.provider).stop({
|
||||
taskId: pending.taskId,
|
||||
pending,
|
||||
});
|
||||
|
|
@ -4406,7 +4367,7 @@ export class ClawdbotAgentService {
|
|||
return;
|
||||
}
|
||||
|
||||
await this.resolveProviderAdapter(pending.provider).stop({
|
||||
await this.providerAdapters.resolve(pending.provider).stop({
|
||||
taskId: pending.taskId,
|
||||
pending,
|
||||
});
|
||||
|
|
@ -5199,7 +5160,7 @@ export class ClawdbotAgentService {
|
|||
.map((event) => `- ${event.message}`)
|
||||
.join('\n')}\n`
|
||||
);
|
||||
await this.resolveProviderAdapter(pending.provider).stop({ taskId, pending });
|
||||
await this.providerAdapters.resolve(pending.provider).stop({ taskId, pending });
|
||||
return {
|
||||
status: 'interrupted',
|
||||
terminalSource: 'operator-interruption',
|
||||
|
|
@ -5389,7 +5350,7 @@ export class ClawdbotAgentService {
|
|||
const health = await this.assertAgentAvailable(agent, agentConfig);
|
||||
return this.dependencyExecution.execute(
|
||||
providerDependencyIdentity(provider, agentConfig.model),
|
||||
() => this.resolveProviderAdapter(provider, surface).probe({ agentConfig, health }),
|
||||
() => this.providerAdapters.resolve(provider, surface).probe({ agentConfig, health }),
|
||||
providerDependencyExecutionOptions
|
||||
);
|
||||
}
|
||||
|
|
@ -5427,24 +5388,40 @@ export class ClawdbotAgentService {
|
|||
return health;
|
||||
}
|
||||
|
||||
private resolveProviderAdapter(
|
||||
provider: ExecutableAgentProvider,
|
||||
surface: ProviderRuntimeSurface = 'task'
|
||||
): AgentProviderAdapter {
|
||||
const definition = getProviderRuntimeAdapterDefinition(provider, surface);
|
||||
const probe = (context: AgentProviderProbeContext) =>
|
||||
this.providerRuntimeManifests.probe(
|
||||
buildProviderRuntimeProbeRequest(provider, context, definition)
|
||||
);
|
||||
|
||||
if (provider === 'codex-cli') {
|
||||
return {
|
||||
id: definition.id,
|
||||
label: definition.label,
|
||||
renderTaskEnvelope: renderCodexCliTaskEnvelope,
|
||||
probe,
|
||||
runEventMapper: getProviderRunEventMapper(provider),
|
||||
start: async ({
|
||||
private createProviderAdapterRegistry(): AgentProviderAdapterRegistry {
|
||||
const host: AgentProviderAdapterHost = {
|
||||
probe: (provider, context, definition) =>
|
||||
this.providerRuntimeManifests.probe(
|
||||
buildProviderRuntimeProbeRequest(provider, context, definition)
|
||||
),
|
||||
probeAcp: (context, definition) => this.probeAcpProviderRuntime(context, definition),
|
||||
assertTransport: (provider, transport, manifest) =>
|
||||
this.assertProviderAdapterTransport(provider, transport, manifest),
|
||||
getPending: (taskId) => pendingAgents.get(taskId),
|
||||
startCodexCli: ({
|
||||
task,
|
||||
agentConfig,
|
||||
transport,
|
||||
logPath,
|
||||
attemptId,
|
||||
startedAt,
|
||||
emitter,
|
||||
sandboxPolicy,
|
||||
runLaunchManifest,
|
||||
}) =>
|
||||
this.startCodexCli(
|
||||
task,
|
||||
agentConfig,
|
||||
transport.content,
|
||||
logPath,
|
||||
attemptId,
|
||||
startedAt,
|
||||
emitter,
|
||||
sandboxPolicy,
|
||||
runLaunchManifest
|
||||
),
|
||||
startCodexSdk: (
|
||||
{
|
||||
task,
|
||||
agentConfig,
|
||||
transport,
|
||||
|
|
@ -5454,375 +5431,222 @@ export class ClawdbotAgentService {
|
|||
emitter,
|
||||
sandboxPolicy,
|
||||
runLaunchManifest,
|
||||
}) => {
|
||||
this.assertProviderAdapterTransport(provider, transport, runLaunchManifest);
|
||||
await this.startCodexCli(
|
||||
task,
|
||||
agentConfig,
|
||||
transport.content,
|
||||
logPath,
|
||||
attemptId,
|
||||
startedAt,
|
||||
emitter,
|
||||
sandboxPolicy,
|
||||
runLaunchManifest
|
||||
);
|
||||
},
|
||||
stop: ({ pending }) => {
|
||||
if (pending.process && !pending.process.killed) pending.process.kill('SIGTERM');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (provider === 'codex-sdk') {
|
||||
return {
|
||||
id: definition.id,
|
||||
label: definition.label,
|
||||
renderTaskEnvelope: renderCodexSdkTaskEnvelope,
|
||||
probe,
|
||||
runEventMapper: getProviderRunEventMapper(provider),
|
||||
start: async ({
|
||||
abortController
|
||||
) =>
|
||||
this.startCodexSdk(
|
||||
task,
|
||||
agentConfig,
|
||||
transport,
|
||||
transport.content,
|
||||
logPath,
|
||||
attemptId,
|
||||
startedAt,
|
||||
emitter,
|
||||
abortController,
|
||||
sandboxPolicy,
|
||||
runLaunchManifest
|
||||
),
|
||||
handleCodexSdkError: (context, abortController, error) =>
|
||||
this.handleCodexSdkAdapterError(context, abortController, error),
|
||||
startCodexAppServer: ({
|
||||
task,
|
||||
agentConfig,
|
||||
transport,
|
||||
logPath,
|
||||
attemptId,
|
||||
startedAt,
|
||||
emitter,
|
||||
sandboxPolicy,
|
||||
runLaunchManifest,
|
||||
}) =>
|
||||
this.startCodexAppServer(
|
||||
task,
|
||||
agentConfig,
|
||||
transport.content,
|
||||
logPath,
|
||||
attemptId,
|
||||
startedAt,
|
||||
emitter,
|
||||
sandboxPolicy,
|
||||
runLaunchManifest,
|
||||
}) => {
|
||||
this.assertProviderAdapterTransport(provider, transport, runLaunchManifest);
|
||||
const abortController = new AbortController();
|
||||
const pending = pendingAgents.get(task.id);
|
||||
if (pending) pending.abortController = abortController;
|
||||
void this.startCodexSdk(
|
||||
task,
|
||||
agentConfig,
|
||||
transport.content,
|
||||
logPath,
|
||||
attemptId,
|
||||
startedAt,
|
||||
emitter,
|
||||
abortController,
|
||||
sandboxPolicy,
|
||||
runLaunchManifest
|
||||
).catch(async (error: unknown) => {
|
||||
const current = pendingAgents.get(task.id);
|
||||
if (!current || current.attemptId !== attemptId) return;
|
||||
if (error instanceof CompletionPersistenceError) {
|
||||
if (emitter.listenerCount('error') > 0) {
|
||||
emitter.emit('error', error.persistenceCause);
|
||||
}
|
||||
log.error(
|
||||
{ err: error.persistenceCause, taskId: task.id, attemptId },
|
||||
'Codex SDK completion could not be persisted after bounded retries'
|
||||
);
|
||||
return;
|
||||
}
|
||||
abortController.abort();
|
||||
const message = this.redactTraceText(
|
||||
error instanceof Error ? error.message : 'Codex SDK attempt failed'
|
||||
);
|
||||
try {
|
||||
const journalEvent = await this.appendRunEvent(
|
||||
task.id,
|
||||
attemptId,
|
||||
'run.error',
|
||||
{ summary: message, error: message, phase: 'stream' },
|
||||
{
|
||||
provider: 'codex-sdk',
|
||||
adapter: 'codex-sdk',
|
||||
agent: agentConfig?.type || 'codex-sdk',
|
||||
model: agentConfig?.model,
|
||||
}
|
||||
);
|
||||
this.emitJournalOutput(journalEvent);
|
||||
await this.appendLog(logPath, `\n## Codex SDK Error\n\n${message}\n`);
|
||||
} catch (logError) {
|
||||
log.error(
|
||||
{ err: logError, taskId: task.id },
|
||||
'Failed to record Codex SDK error evidence'
|
||||
);
|
||||
}
|
||||
try {
|
||||
await this.completeAgent(
|
||||
task.id,
|
||||
{ success: false, error: message },
|
||||
{
|
||||
attemptId,
|
||||
terminalSource: 'stream',
|
||||
providerRuntimeManifestDigest: current.providerRuntimeManifest.digest,
|
||||
}
|
||||
);
|
||||
} catch (finalizationError) {
|
||||
const retryable =
|
||||
current.preparedCompletion !== undefined &&
|
||||
!(finalizationError instanceof CompletionOwnershipError);
|
||||
if (!retryable && pendingAgents.get(task.id)?.attemptId === attemptId) {
|
||||
pendingAgents.delete(task.id);
|
||||
}
|
||||
if (emitter.listenerCount('error') > 0) {
|
||||
emitter.emit('error', finalizationError);
|
||||
}
|
||||
log.error(
|
||||
{ err: finalizationError, taskId: task.id, attemptId, retryable },
|
||||
retryable
|
||||
? 'Codex SDK failure completion remains pending after bounded persistence retries'
|
||||
: 'Codex SDK failure could not update stale persisted attempt state'
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
stop: ({ pending }) => {
|
||||
pending.abortController?.abort();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (provider === 'codex-app-server') {
|
||||
return {
|
||||
id: definition.id,
|
||||
label: definition.label,
|
||||
renderTaskEnvelope: renderCodexAppServerTaskEnvelope,
|
||||
probe,
|
||||
runEventMapper: getProviderRunEventMapper(provider),
|
||||
start: async ({
|
||||
runLaunchManifest
|
||||
),
|
||||
startAcpStdio: ({
|
||||
task,
|
||||
agentConfig,
|
||||
transport,
|
||||
logPath,
|
||||
attemptId,
|
||||
sandboxPolicy,
|
||||
runLaunchManifest,
|
||||
conversation,
|
||||
}) =>
|
||||
this.startAcpStdio(
|
||||
task,
|
||||
agentConfig,
|
||||
transport,
|
||||
transport.content,
|
||||
logPath,
|
||||
attemptId,
|
||||
sandboxPolicy,
|
||||
runLaunchManifest,
|
||||
conversation
|
||||
),
|
||||
startClaudeCode: ({
|
||||
task,
|
||||
agentConfig,
|
||||
transport,
|
||||
logPath,
|
||||
attemptId,
|
||||
startedAt,
|
||||
emitter,
|
||||
sandboxPolicy,
|
||||
runLaunchManifest,
|
||||
}) =>
|
||||
this.startClaudeCode(
|
||||
task,
|
||||
agentConfig,
|
||||
transport.content,
|
||||
logPath,
|
||||
attemptId,
|
||||
startedAt,
|
||||
emitter,
|
||||
sandboxPolicy,
|
||||
runLaunchManifest,
|
||||
}) => {
|
||||
this.assertProviderAdapterTransport(provider, transport, runLaunchManifest);
|
||||
await this.startCodexAppServer(
|
||||
task,
|
||||
agentConfig,
|
||||
transport.content,
|
||||
logPath,
|
||||
attemptId,
|
||||
startedAt,
|
||||
emitter,
|
||||
sandboxPolicy,
|
||||
runLaunchManifest
|
||||
);
|
||||
},
|
||||
stop: async ({ pending }) => {
|
||||
try {
|
||||
await pending.codexAppServerControl?.interrupt();
|
||||
} catch (error) {
|
||||
log.warn(
|
||||
{ err: error, taskId: pending.taskId },
|
||||
'Codex app-server cooperative interrupt failed; closing the supervised process'
|
||||
);
|
||||
}
|
||||
pending.codexAppServerControl?.close();
|
||||
const child = pending.process;
|
||||
if (!child || child.exitCode != null || child.signalCode != null) return;
|
||||
const forcedStop = setTimeout(() => {
|
||||
if (child.exitCode == null && child.signalCode == null) child.kill('SIGKILL');
|
||||
}, 5_000);
|
||||
child.once('close', () => clearTimeout(forcedStop));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (provider === 'acp-stdio') {
|
||||
return {
|
||||
id: definition.id,
|
||||
label: definition.label,
|
||||
renderTaskEnvelope: renderAcpStdioTaskEnvelope,
|
||||
probe: (context) => this.probeAcpProviderRuntime(context, definition),
|
||||
runEventMapper: getProviderRunEventMapper(provider),
|
||||
start: async ({
|
||||
runLaunchManifest
|
||||
),
|
||||
startHermesCli: ({
|
||||
task,
|
||||
agentConfig,
|
||||
transport,
|
||||
logPath,
|
||||
attemptId,
|
||||
startedAt,
|
||||
emitter,
|
||||
sandboxPolicy,
|
||||
}) =>
|
||||
this.startHermesCli(
|
||||
task,
|
||||
agentConfig,
|
||||
transport,
|
||||
logPath,
|
||||
attemptId,
|
||||
sandboxPolicy,
|
||||
runLaunchManifest,
|
||||
conversation,
|
||||
}) => {
|
||||
this.assertProviderAdapterTransport(provider, transport, runLaunchManifest);
|
||||
await this.startAcpStdio(
|
||||
task,
|
||||
agentConfig,
|
||||
transport.content,
|
||||
logPath,
|
||||
attemptId,
|
||||
sandboxPolicy,
|
||||
runLaunchManifest,
|
||||
conversation
|
||||
);
|
||||
},
|
||||
stop: async ({ pending }) => {
|
||||
pending.abortController?.abort();
|
||||
await pending.acpControl?.cancel().catch(() => undefined);
|
||||
await pending.acpControl?.close().catch(() => undefined);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (provider === 'claude-code') {
|
||||
return {
|
||||
id: definition.id,
|
||||
label: definition.label,
|
||||
renderTaskEnvelope: renderClaudeCodeTaskEnvelope,
|
||||
probe,
|
||||
runEventMapper: getProviderRunEventMapper(provider),
|
||||
start: async ({
|
||||
task,
|
||||
agentConfig,
|
||||
transport,
|
||||
transport.content,
|
||||
logPath,
|
||||
attemptId,
|
||||
startedAt,
|
||||
emitter,
|
||||
sandboxPolicy,
|
||||
runLaunchManifest,
|
||||
}) => {
|
||||
this.assertProviderAdapterTransport(provider, transport, runLaunchManifest);
|
||||
await this.startClaudeCode(
|
||||
task,
|
||||
agentConfig,
|
||||
transport.content,
|
||||
logPath,
|
||||
attemptId,
|
||||
startedAt,
|
||||
emitter,
|
||||
sandboxPolicy,
|
||||
runLaunchManifest
|
||||
);
|
||||
},
|
||||
stop: ({ pending }) => {
|
||||
const child = pending.process;
|
||||
if (!child || child.exitCode != null || child.signalCode != null) return;
|
||||
child.kill('SIGTERM');
|
||||
const forcedStop = setTimeout(() => {
|
||||
if (child.exitCode == null && child.signalCode == null) {
|
||||
child.kill('SIGKILL');
|
||||
log.warn(
|
||||
{ taskId: pending.taskId },
|
||||
'[ClawdbotAgent] Claude Code SIGKILL issued after graceful stop timeout'
|
||||
);
|
||||
}
|
||||
}, 5_000);
|
||||
child.once('close', () => clearTimeout(forcedStop));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (provider === 'hermes-cli') {
|
||||
return {
|
||||
id: definition.id,
|
||||
label: definition.label,
|
||||
renderTaskEnvelope: renderHermesTaskEnvelope,
|
||||
probe,
|
||||
runEventMapper: getProviderRunEventMapper(provider),
|
||||
start: async ({
|
||||
task,
|
||||
agentConfig,
|
||||
transport,
|
||||
logPath,
|
||||
attemptId,
|
||||
startedAt,
|
||||
emitter,
|
||||
sandboxPolicy,
|
||||
runLaunchManifest,
|
||||
}) => {
|
||||
this.assertProviderAdapterTransport(provider, transport, runLaunchManifest);
|
||||
await this.startHermesCli(
|
||||
task,
|
||||
agentConfig,
|
||||
transport.content,
|
||||
logPath,
|
||||
attemptId,
|
||||
startedAt,
|
||||
emitter,
|
||||
sandboxPolicy
|
||||
);
|
||||
},
|
||||
stop: ({ pending }) => {
|
||||
if (pending.process && !pending.process.killed) {
|
||||
pending.process.kill('SIGTERM');
|
||||
// Bounded forced-stop: send SIGKILL after 5 s if the process is still running
|
||||
const forcedStop = setTimeout(() => {
|
||||
if (pending.process && !pending.process.killed) {
|
||||
pending.process.kill('SIGKILL');
|
||||
log.warn(
|
||||
{ taskId: pending.taskId },
|
||||
'[ClawdbotAgent] Hermes SIGKILL issued after graceful stop timeout'
|
||||
);
|
||||
}
|
||||
}, 5_000);
|
||||
pending.process.once('close', () => clearTimeout(forcedStop));
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: definition.id,
|
||||
label: definition.label,
|
||||
renderTaskEnvelope: renderOpenClawTaskEnvelope,
|
||||
probe,
|
||||
runEventMapper: getProviderRunEventMapper(provider),
|
||||
start: async ({ transport, task, attemptId, agentConfig, runLaunchManifest }) => {
|
||||
this.assertProviderAdapterTransport(provider, transport, runLaunchManifest);
|
||||
// Use the HTTP gateway adapter (sessions_spawn) instead of writing a request file.
|
||||
// The real spawn acknowledgement surfaces policy denial or gateway
|
||||
// unreachability, which the caller's error handler rolls back to 'todo'.
|
||||
const openclawAdapter = new HttpOpenClawTaskAdapter();
|
||||
const result = await openclawAdapter.spawnTask({
|
||||
taskId: task.id,
|
||||
attemptId,
|
||||
agentId: agentConfig?.type || 'openclaw',
|
||||
agentName: agentConfig?.name,
|
||||
model: agentConfig?.model,
|
||||
prompt: transport.content,
|
||||
timeoutSeconds: 900,
|
||||
});
|
||||
await this.attemptLifecycle.patchActiveAttempt(task.id, attemptId, {
|
||||
sessionKey: result.sessionKey,
|
||||
});
|
||||
await this.recordConversationIdentity(task.id, attemptId, {
|
||||
conversationId: result.sessionKey,
|
||||
});
|
||||
void this.recordAgentStarted(
|
||||
task,
|
||||
attemptId,
|
||||
agentConfig?.type || 'openclaw',
|
||||
'openclaw',
|
||||
agentConfig
|
||||
);
|
||||
const pending = pendingAgents.get(task.id);
|
||||
if (!pending || pending.attemptId !== attemptId || !pending.supervisorId) {
|
||||
throw new ConflictError('OpenClaw session has no durable run supervisor binding.', {
|
||||
taskId: task.id,
|
||||
attemptId,
|
||||
});
|
||||
}
|
||||
pending.openclawSessionKey = result.sessionKey;
|
||||
await this.runSupervisor.attachRemoteSession(pending.supervisorId, result.sessionKey);
|
||||
log.info(
|
||||
{ taskId: task.id, attemptId, sessionKey: result.sessionKey },
|
||||
'[ClawdbotAgent] OpenClaw session spawned via gateway'
|
||||
);
|
||||
},
|
||||
stop: async ({ pending }) => {
|
||||
// OpenClaw does not expose a direct stop API for sub-sessions in v2026.6.11.
|
||||
// Completion is driven by the callback URL included in the task prompt.
|
||||
log.warn(
|
||||
{ taskId: pending.taskId, sessionKey: pending.openclawSessionKey },
|
||||
'[ClawdbotAgent] OpenClaw stop requested; sub-session will complete via callback'
|
||||
);
|
||||
},
|
||||
sandboxPolicy
|
||||
),
|
||||
startOpenClaw: (context) => this.startOpenClawAdapter(context),
|
||||
warn: (details, message) => log.warn(details, message),
|
||||
};
|
||||
return new AgentProviderAdapterRegistry(host);
|
||||
}
|
||||
|
||||
private async handleCodexSdkAdapterError(
|
||||
context: AgentProviderStartContext,
|
||||
abortController: AbortController,
|
||||
error: unknown
|
||||
): Promise<void> {
|
||||
const { task, attemptId, emitter, logPath, agentConfig } = context;
|
||||
const current = pendingAgents.get(task.id);
|
||||
if (!current || current.attemptId !== attemptId) return;
|
||||
if (error instanceof CompletionPersistenceError) {
|
||||
if (emitter.listenerCount('error') > 0) {
|
||||
emitter.emit('error', error.persistenceCause);
|
||||
}
|
||||
log.error(
|
||||
{ err: error.persistenceCause, taskId: task.id, attemptId },
|
||||
'Codex SDK completion could not be persisted after bounded retries'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
abortController.abort();
|
||||
const message = this.redactTraceText(
|
||||
error instanceof Error ? error.message : 'Codex SDK attempt failed'
|
||||
);
|
||||
try {
|
||||
const journalEvent = await this.appendRunEvent(
|
||||
task.id,
|
||||
attemptId,
|
||||
'run.error',
|
||||
{ summary: message, error: message, phase: 'stream' },
|
||||
{
|
||||
provider: 'codex-sdk',
|
||||
adapter: 'codex-sdk',
|
||||
agent: agentConfig?.type || 'codex-sdk',
|
||||
model: agentConfig?.model,
|
||||
}
|
||||
);
|
||||
this.emitJournalOutput(journalEvent);
|
||||
await this.appendLog(logPath, `\n## Codex SDK Error\n\n${message}\n`);
|
||||
} catch (logError) {
|
||||
log.error({ err: logError, taskId: task.id }, 'Failed to record Codex SDK error evidence');
|
||||
}
|
||||
|
||||
try {
|
||||
await this.completeAgent(
|
||||
task.id,
|
||||
{ success: false, error: message },
|
||||
{
|
||||
attemptId,
|
||||
terminalSource: 'stream',
|
||||
providerRuntimeManifestDigest: current.providerRuntimeManifest.digest,
|
||||
}
|
||||
);
|
||||
} catch (finalizationError) {
|
||||
const retryable =
|
||||
current.preparedCompletion !== undefined &&
|
||||
!(finalizationError instanceof CompletionOwnershipError);
|
||||
if (!retryable && pendingAgents.get(task.id)?.attemptId === attemptId) {
|
||||
pendingAgents.delete(task.id);
|
||||
}
|
||||
if (emitter.listenerCount('error') > 0) {
|
||||
emitter.emit('error', finalizationError);
|
||||
}
|
||||
log.error(
|
||||
{ err: finalizationError, taskId: task.id, attemptId, retryable },
|
||||
retryable
|
||||
? 'Codex SDK failure completion remains pending after bounded persistence retries'
|
||||
: 'Codex SDK failure could not update stale persisted attempt state'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async startOpenClawAdapter(context: AgentProviderStartContext): Promise<void> {
|
||||
const { transport, task, attemptId, agentConfig } = context;
|
||||
const openclawAdapter = new HttpOpenClawTaskAdapter();
|
||||
const result = await openclawAdapter.spawnTask({
|
||||
taskId: task.id,
|
||||
attemptId,
|
||||
agentId: agentConfig?.type || 'openclaw',
|
||||
agentName: agentConfig?.name,
|
||||
model: agentConfig?.model,
|
||||
prompt: transport.content,
|
||||
timeoutSeconds: 900,
|
||||
});
|
||||
await this.attemptLifecycle.patchActiveAttempt(task.id, attemptId, {
|
||||
sessionKey: result.sessionKey,
|
||||
});
|
||||
await this.recordConversationIdentity(task.id, attemptId, {
|
||||
conversationId: result.sessionKey,
|
||||
});
|
||||
void this.recordAgentStarted(
|
||||
task,
|
||||
attemptId,
|
||||
agentConfig?.type || 'openclaw',
|
||||
'openclaw',
|
||||
agentConfig
|
||||
);
|
||||
const pending = pendingAgents.get(task.id);
|
||||
if (!pending || pending.attemptId !== attemptId || !pending.supervisorId) {
|
||||
throw new ConflictError('OpenClaw session has no durable run supervisor binding.', {
|
||||
taskId: task.id,
|
||||
attemptId,
|
||||
});
|
||||
}
|
||||
pending.openclawSessionKey = result.sessionKey;
|
||||
await this.runSupervisor.attachRemoteSession(pending.supervisorId, result.sessionKey);
|
||||
log.info(
|
||||
{ taskId: task.id, attemptId, sessionKey: result.sessionKey },
|
||||
'[ClawdbotAgent] OpenClaw session spawned via gateway'
|
||||
);
|
||||
}
|
||||
|
||||
private assertProviderAdapterLaunchManifest(
|
||||
|
|
@ -7153,11 +6977,13 @@ export class ClawdbotAgentService {
|
|||
attemptId,
|
||||
agentConfig,
|
||||
'codex-app-server',
|
||||
this.resolveProviderAdapter('codex-app-server').runEventMapper.mapEvent(
|
||||
classified.providerType,
|
||||
recordValueForProvider(record, 'params'),
|
||||
classified.summary
|
||||
)
|
||||
this.providerAdapters
|
||||
.resolve('codex-app-server')
|
||||
.runEventMapper.mapEvent(
|
||||
classified.providerType,
|
||||
recordValueForProvider(record, 'params'),
|
||||
classified.summary
|
||||
)
|
||||
);
|
||||
this.emitJournalOutput(journalEvent);
|
||||
if (classified.usage) {
|
||||
|
|
@ -7264,11 +7090,9 @@ export class ClawdbotAgentService {
|
|||
attemptId,
|
||||
agentConfig,
|
||||
'codex-app-server',
|
||||
this.resolveProviderAdapter('codex-app-server').runEventMapper.mapEvent(
|
||||
method,
|
||||
recordValueForProvider(record, 'params'),
|
||||
summary
|
||||
)
|
||||
this.providerAdapters
|
||||
.resolve('codex-app-server')
|
||||
.runEventMapper.mapEvent(method, recordValueForProvider(record, 'params'), summary)
|
||||
);
|
||||
this.emitJournalOutput(requested);
|
||||
const resolved = await this.appendRunEvent(
|
||||
|
|
@ -7723,11 +7547,9 @@ export class ClawdbotAgentService {
|
|||
attemptId,
|
||||
agentConfig,
|
||||
'claude-code',
|
||||
this.resolveProviderAdapter('claude-code').runEventMapper.mapEvent(
|
||||
classified.providerType,
|
||||
record,
|
||||
classified.summary
|
||||
)
|
||||
this.providerAdapters
|
||||
.resolve('claude-code')
|
||||
.runEventMapper.mapEvent(classified.providerType, record, classified.summary)
|
||||
);
|
||||
this.emitJournalOutput(journalEvent);
|
||||
if (classified.usage) {
|
||||
|
|
@ -8741,7 +8563,7 @@ export class ClawdbotAgentService {
|
|||
): Promise<void> {
|
||||
const content = this.redactTraceText(chunk.trimEnd());
|
||||
if (!content.trim()) return;
|
||||
const mapper = this.resolveProviderAdapter(provider).runEventMapper;
|
||||
const mapper = this.providerAdapters.resolve(provider).runEventMapper;
|
||||
const event = await this.appendMappedProviderEvent(
|
||||
task,
|
||||
attemptId,
|
||||
|
|
@ -8823,7 +8645,7 @@ export class ClawdbotAgentService {
|
|||
attemptId,
|
||||
agentConfig,
|
||||
provider,
|
||||
this.resolveProviderAdapter(provider).runEventMapper.mapEvent(type, event, sanitizedSummary)
|
||||
this.providerAdapters.resolve(provider).runEventMapper.mapEvent(type, event, sanitizedSummary)
|
||||
);
|
||||
this.emitJournalOutput(journalEvent);
|
||||
if (usage) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue