Files
openclaw/src/plugin-sdk/agent-harness-task-runtime.ts
Peter Steinberger 77d9ac30bb refactor: reuse shared coercion helpers (#86419)
* refactor: share talk event metric extraction

* refactor: reuse shared coercion helpers

* refactor: reuse shared primitive guards

* refactor: reuse shared record guard

* refactor: reuse shared primitive helpers

* refactor: reuse shared string guards

* refactor: reuse shared non-empty string guard

* refactor: share plugin primitive coercion helpers

* refactor: reuse plugin coercion helpers

* refactor: reuse plugin coercion helpers in more plugins

* refactor: reuse channel coercion helpers

* refactor: reuse monitor coercion helpers

* refactor: reuse provider coercion helpers

* refactor: reuse core coercion helpers

* refactor: reuse runtime coercion helpers

* refactor: reuse helper coercion in codex paths

* refactor: reuse helper coercion in runtime paths

* refactor: reuse codex app-server coercion helpers

* refactor: reuse codex record helpers

* refactor: reuse migration and qa record helpers

* refactor: reuse feishu and core helper guards

* refactor: reuse browser and policy coercion helpers

* refactor: reuse memory wiki record helper

* refactor: share boolean coercion helpers

* refactor: reuse finite number coercion

* refactor: reuse trimmed string list helpers

* refactor: reuse string list normalization

* refactor: reuse remaining string list helpers

* refactor: reuse string entry normalizer

* refactor: share sorted string helpers

* refactor: share string list normalization

* test: preserve command registry browser imports

* refactor: reuse trimmed list helpers

* refactor: reuse string dedupe helpers

* refactor: reuse local dedupe helpers

* refactor: reuse more string dedupe helpers

* refactor: reuse command string dedupe helpers

* refactor: dedupe memory path lists with helper

* refactor: expose string dedupe helpers to plugins

* refactor: reuse core string dedupe helpers

* refactor: reuse shared unique value helpers

* refactor: reuse unique helpers in agent utilities

* refactor: reuse unique helpers in config plumbing

* refactor: reuse unique helpers in extensions

* refactor: reuse unique helpers in core utilities

* refactor: reuse unique helpers in qa plugins

* refactor: reuse unique helpers in memory plugins

* refactor: reuse unique helpers in channel plugins

* refactor: reuse unique helpers in core tails

* refactor: reuse unique helper in comfy workflow

* refactor: reuse unique helpers in test utilities

* refactor: expose unique value helper to plugins

* refactor: reuse unique helpers for numeric lists

* refactor: replace index dedupe filters

* refactor: reuse string entry normalization

* refactor: reuse string normalization in plugin helpers

* refactor: reuse string normalization in extension helpers

* refactor: reuse string normalization in channel parsers

* refactor: reuse string normalization in memory search

* refactor: reuse string normalization in provider parsers

* refactor: reuse string normalization in qa helpers

* refactor: reuse string normalization in infra parsers

* refactor: reuse string normalization in messaging parsers

* refactor: reuse string normalization in core parsers

* refactor: reuse string normalization in extension parsers

* refactor: reuse string normalization in remaining parsers

* refactor: reuse string normalization in final parser spots

* refactor: reuse string normalization in qa media helpers

* refactor: reuse normalization in provider and media lists

* refactor: reuse normalization for remaining set filters

* refactor: reuse normalization in policy allowlists

* refactor: reuse normalization in session and owner lists

* refactor: centralize primitive string lists

* refactor: reuse lowercase entry helpers

* refactor: reuse sorted string helpers

* refactor: reuse unique trimmed helpers

* refactor: reuse string normalization helpers

* refactor: reuse catalog string helpers

* refactor: reuse remaining string helpers

* refactor: simplify remaining list normalization

* refactor: reuse codex auth order normalization

* chore: refresh plugin sdk api baseline

* fix: make shared string sorting deterministic

* chore: refresh plugin sdk api baseline

* fix: align host env security ordering
2026-05-25 21:20:41 +01:00

262 lines
8.9 KiB
TypeScript

import { buildAnnounceIdempotencyKey } from "../agents/announce-idempotency.js";
import {
AGENT_INTERNAL_EVENT_TYPE_TASK_COMPLETION,
type AgentInternalEventStatus,
} from "../agents/internal-event-contract.js";
import {
formatAgentInternalEventsForPrompt,
type AgentInternalEvent,
} from "../agents/internal-events.js";
import {
deliverSubagentAnnouncement,
isInternalAnnounceRequesterSession,
loadRequesterSessionEntry,
resolveSubagentCompletionOrigin,
} from "../agents/subagent-announce-delivery.js";
import { resolveAnnounceOrigin } from "../agents/subagent-announce-origin.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import {
assertAgentHarnessTaskRuntimeScope,
type AgentHarnessTaskRuntimeScope,
} from "../tasks/agent-harness-task-runtime-scope.js";
import {
createRunningTaskRun,
finalizeTaskRunByRunId,
recordTaskRunProgressByRunId,
setDetachedTaskDeliveryStatusByRunId,
} from "../tasks/detached-task-runtime.js";
import { listTaskRecords, type TaskRecord } from "../tasks/runtime-internal.js";
import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js";
export type { TaskRecord as AgentHarnessTaskRecord };
export type { AgentHarnessTaskRuntimeScope };
type AgentHarnessTaskRuntimeId = Parameters<typeof createRunningTaskRun>[0]["runtime"];
type CreateRunningTaskRunParams = Parameters<typeof createRunningTaskRun>[0];
type RecordTaskRunProgressParams = Parameters<typeof recordTaskRunProgressByRunId>[0];
type FinalizeTaskRunParams = Parameters<typeof finalizeTaskRunByRunId>[0];
type SetDeliveryStatusParams = Parameters<typeof setDetachedTaskDeliveryStatusByRunId>[0];
export type AgentHarnessTaskRuntimeScopeParams = {
runtime: AgentHarnessTaskRuntimeId;
scope: AgentHarnessTaskRuntimeScope;
taskKind?: string;
runIdPrefix?: string;
};
export type AgentHarnessScopedCreateRunningTaskRunParams = Omit<
CreateRunningTaskRunParams,
"runtime" | "taskKind" | "requesterSessionKey" | "ownerKey" | "scopeKind"
> & {
runId: string;
};
export type AgentHarnessScopedRecordTaskRunProgressParams = Omit<
RecordTaskRunProgressParams,
"runtime" | "sessionKey"
>;
export type AgentHarnessScopedFinalizeTaskRunParams = Omit<
FinalizeTaskRunParams,
"runtime" | "sessionKey"
>;
export type AgentHarnessScopedSetDeliveryStatusParams = Omit<
SetDeliveryStatusParams,
"runtime" | "sessionKey"
>;
export type AgentHarnessTaskRuntime = {
createRunningTaskRun(params: AgentHarnessScopedCreateRunningTaskRunParams): TaskRecord;
recordTaskRunProgressByRunId(params: AgentHarnessScopedRecordTaskRunProgressParams): TaskRecord[];
finalizeTaskRunByRunId(params: AgentHarnessScopedFinalizeTaskRunParams): TaskRecord[];
setDetachedTaskDeliveryStatusByRunId(
params: AgentHarnessScopedSetDeliveryStatusParams,
): TaskRecord[];
listTaskRecords(): TaskRecord[];
};
export type AgentHarnessCompletionStatus = "succeeded" | "failed" | "cancelled";
export type AgentHarnessCompletionDelivery = Awaited<
ReturnType<typeof deliverSubagentAnnouncement>
>;
const AGENT_HARNESS_COMPLETION_SOURCE_TOOL = "agent_harness_task";
export function createAgentHarnessTaskRuntime(
params: AgentHarnessTaskRuntimeScopeParams,
): AgentHarnessTaskRuntime {
const runtime = params.runtime;
const scope = assertAgentHarnessTaskRuntimeScope(params.scope);
const requesterSessionKey = scope.requesterSessionKey;
const taskKind = normalizeOptionalString(params.taskKind);
const runIdPrefix = normalizeOptionalString(params.runIdPrefix);
const assertRunId = (runId: string) => assertScopedRunId(runId, runIdPrefix);
return {
createRunningTaskRun(taskParams) {
assertRunId(taskParams.runId);
return createRunningTaskRun({
...taskParams,
runtime,
...(taskKind ? { taskKind } : {}),
requesterSessionKey,
ownerKey: requesterSessionKey,
scopeKind: "session",
});
},
recordTaskRunProgressByRunId(taskParams) {
assertRunId(taskParams.runId);
return recordTaskRunProgressByRunId({
...taskParams,
runtime,
sessionKey: requesterSessionKey,
});
},
finalizeTaskRunByRunId(taskParams) {
assertRunId(taskParams.runId);
return finalizeTaskRunByRunId({
...taskParams,
runtime,
sessionKey: requesterSessionKey,
});
},
setDetachedTaskDeliveryStatusByRunId(taskParams) {
assertRunId(taskParams.runId);
return setDetachedTaskDeliveryStatusByRunId({
...taskParams,
runtime,
sessionKey: requesterSessionKey,
});
},
listTaskRecords() {
return listTaskRecords().filter(
(task) =>
task.runtime === runtime &&
(!taskKind || task.taskKind === taskKind) &&
task.scopeKind === "session" &&
task.ownerKey === requesterSessionKey &&
(!runIdPrefix || task.runId?.startsWith(runIdPrefix)),
);
},
};
}
export async function deliverAgentHarnessTaskCompletion(params: {
scope: AgentHarnessTaskRuntimeScope;
childSessionKey: string;
childSessionId: string;
announceId: string;
status: AgentHarnessCompletionStatus;
statusLabel?: string;
result: string;
taskLabel?: string;
announceType?: string;
replyInstruction?: string;
signal?: AbortSignal;
}): Promise<AgentHarnessCompletionDelivery> {
const scope = assertAgentHarnessTaskRuntimeScope(params.scope);
const requesterSessionKey = scope.requesterSessionKey;
const childSessionKey = params.childSessionKey.trim();
const childSessionId = params.childSessionId.trim();
const taskLabel = params.taskLabel?.trim() || "Agent harness task";
const announceType = params.announceType?.trim() || "Agent harness task";
const statusLabel = params.statusLabel?.trim() || params.status;
const eventStatus = mapHarnessCompletionStatus(params.status);
const requesterIsSubagent = isInternalAnnounceRequesterSession(requesterSessionKey);
let directOrigin = scope.requesterOrigin;
if (!requesterIsSubagent) {
const { entry } = loadRequesterSessionEntry(requesterSessionKey);
directOrigin = resolveAnnounceOrigin(entry, scope.requesterOrigin);
}
const completionDirectOrigin =
requesterIsSubagent || !directOrigin
? directOrigin
: await resolveSubagentCompletionOrigin({
childSessionKey,
requesterSessionKey,
requesterOrigin: directOrigin,
childRunId: childSessionKey,
spawnMode: "run",
expectsCompletionMessage: true,
});
const internalEvents: AgentInternalEvent[] = [
{
type: AGENT_INTERNAL_EVENT_TYPE_TASK_COMPLETION,
source: "subagent",
childSessionKey,
childSessionId,
announceType,
taskLabel,
status: eventStatus,
statusLabel,
result: params.result,
replyInstruction:
params.replyInstruction?.trim() ||
"Use the completed harness task result to continue or wrap up the parent task. If this is a channel session, send the visible response with the message tool instead of only writing a transcript final answer.",
},
];
const prompt = formatAgentInternalEventsForPrompt(internalEvents);
return await deliverSubagentAnnouncement({
requesterSessionKey,
announceId: params.announceId,
triggerMessage: prompt,
steerMessage: prompt,
internalEvents,
summaryLine: taskLabel,
requesterSessionOrigin: scope.requesterOrigin,
requesterOrigin: completionDirectOrigin ?? directOrigin,
completionDirectOrigin: completionDirectOrigin ?? directOrigin,
directOrigin,
sourceSessionKey: childSessionKey,
sourceChannel: INTERNAL_MESSAGE_CHANNEL,
sourceTool: AGENT_HARNESS_COMPLETION_SOURCE_TOOL,
targetRequesterSessionKey: requesterSessionKey,
requesterIsSubagent,
expectsCompletionMessage: true,
bestEffortDeliver: true,
directIdempotencyKey: buildAnnounceIdempotencyKey(params.announceId),
signal: params.signal,
});
}
function mapHarnessCompletionStatus(
status: AgentHarnessCompletionStatus,
): AgentInternalEventStatus {
if (status === "succeeded") {
return "ok";
}
return "error";
}
export function isDurableAgentHarnessCompletionDelivery(
delivery: AgentHarnessCompletionDelivery,
): boolean {
if (!delivery.delivered) {
return false;
}
if (delivery.path === "steered") {
return true;
}
if (delivery.path !== "direct") {
return false;
}
const phases = Array.isArray(delivery.phases) ? delivery.phases : undefined;
if (!phases) {
return true;
}
return phases.some(
(phase) => phase.phase === "direct-primary" && phase.delivered && phase.path === "direct",
);
}
function assertScopedRunId(runId: string, runIdPrefix: string | undefined): void {
const normalized = runId.trim();
if (!normalized) {
throw new Error("Agent harness task runtime requires runId");
}
if (runIdPrefix && !normalized.startsWith(runIdPrefix)) {
throw new Error("Agent harness task runId is outside the configured scope");
}
}