File size: 11,570 Bytes
5c2a829 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 | /**
* Runtime SDK helpers for agent harness task persistence and completion delivery.
*/
import { normalizeOptionalString } from "../../packages/normalization-core/src/string-coerce.js";
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,
} from "../agents/subagents/announce/subagent-announce-delivery.js";
import {
resolveAnnounceOrigin,
resolveSubagentCompletionOrigin,
} from "../agents/subagents/announce/subagent-announce-origin.js";
import {
getGatewayContextResolver,
withPluginRuntimeGatewayContextResolver,
} from "../plugins/runtime/gateway-request-scope.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 { captureTaskExecutionOwner } from "../tasks/task-execution-owner.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];
/** Scope and naming options used to bind task operations to one requester session. */
export type AgentHarnessTaskRuntimeScopeParams = {
scope: AgentHarnessTaskRuntimeScope;
runIdPrefix?: string;
/** Local harness process PID, when the transport owns and reports one. */
executionPid?: number;
} & (
| {
// Core identifies harness-owned subagent rows by the taskKind stamped here
// (isHarnessOwnedSubagentTask); a subagent row created without one would be
// read as an OpenClaw-owned child session and reclaimed on the short grace.
runtime: Extract<AgentHarnessTaskRuntimeId, "subagent">;
taskKind: string;
}
| {
runtime: Exclude<AgentHarnessTaskRuntimeId, "subagent">;
taskKind?: string;
}
);
/** Create-task params with runtime and requester scope supplied by the scoped task runtime. */
export type AgentHarnessScopedCreateRunningTaskRunParams = Omit<
CreateRunningTaskRunParams,
"runtime" | "taskKind" | "requesterSessionKey" | "ownerKey" | "scopeKind" | "executionOwner"
> & {
runId: string;
};
/** Progress params scoped to the requester session owned by the harness runtime. */
export type AgentHarnessScopedRecordTaskRunProgressParams = Omit<
RecordTaskRunProgressParams,
"runtime" | "sessionKey"
>;
/** Finalization params scoped to the requester session owned by the harness runtime. */
export type AgentHarnessScopedFinalizeTaskRunParams = Omit<
FinalizeTaskRunParams,
"runtime" | "sessionKey"
>;
/** Delivery-status params scoped to the requester session owned by the harness runtime. */
export type AgentHarnessScopedSetDeliveryStatusParams = Omit<
SetDeliveryStatusParams,
"runtime" | "sessionKey"
>;
/** Scoped task runtime that prevents callers from mutating tasks outside their harness scope. */
export type AgentHarnessTaskRuntime = {
createRunningTaskRun(params: AgentHarnessScopedCreateRunningTaskRunParams): TaskRecord;
tryCreateRunningTaskRun(params: AgentHarnessScopedCreateRunningTaskRunParams): TaskRecord | null;
recordTaskRunProgressByRunId(params: AgentHarnessScopedRecordTaskRunProgressParams): TaskRecord[];
finalizeTaskRunByRunId(params: AgentHarnessScopedFinalizeTaskRunParams): TaskRecord[];
setDetachedTaskDeliveryStatusByRunId(
params: AgentHarnessScopedSetDeliveryStatusParams,
): TaskRecord[];
listTaskRecords(): TaskRecord[];
};
/** Completion states a harness task can report to its requester. */
export type AgentHarnessCompletionStatus = "succeeded" | "failed" | "cancelled";
/** Delivery result returned after routing a harness task completion announcement. */
export type AgentHarnessCompletionDelivery = Awaited<
ReturnType<typeof deliverSubagentAnnouncement>
>;
const AGENT_HARNESS_COMPLETION_SOURCE_TOOL = "agent_harness_task";
/** Creates a task runtime whose run ids and task records are constrained to one scope. */
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);
// Remote and unidentified harnesses must not inherit the Gateway's identity.
const executionOwner =
params.executionPid === undefined ? undefined : captureTaskExecutionOwner(params.executionPid);
const assertRunId = (runId: string) => assertScopedRunId(runId, runIdPrefix);
const tryCreateRunningTaskRun = (
taskParams: AgentHarnessScopedCreateRunningTaskRunParams,
): TaskRecord | null => {
assertRunId(taskParams.runId);
return createRunningTaskRun({
...taskParams,
runtime,
...(taskKind ? { taskKind } : {}),
requesterSessionKey,
ownerKey: requesterSessionKey,
scopeKind: "session",
executionOwner,
});
};
return {
createRunningTaskRun(taskParams) {
const task = tryCreateRunningTaskRun(taskParams);
if (!task) {
throw new Error("Task persistence failed.");
}
return task;
},
tryCreateRunningTaskRun,
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)),
);
},
};
}
/** Delivers a completed harness task result back to the requester or parent session. */
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);
const deliver = () =>
deliverSubagentAnnouncement({
requesterSessionKey,
triggerMessage: prompt,
steerMessage: prompt,
internalEvents,
requesterSessionOrigin: scope.requesterOrigin,
completionDirectOrigin: completionDirectOrigin ?? directOrigin,
directOrigin,
sourceSessionKey: childSessionKey,
sourceTool: AGENT_HARNESS_COMPLETION_SOURCE_TOOL,
targetRequesterSessionKey: requesterSessionKey,
requesterIsSubagent,
expectsCompletionMessage: true,
bestEffortDeliver: true,
directIdempotencyKey: buildAnnounceIdempotencyKey(params.announceId),
signal: params.signal,
});
const resolveGatewayContext = getGatewayContextResolver(scope);
return resolveGatewayContext
? await withPluginRuntimeGatewayContextResolver(resolveGatewayContext, deliver)
: await deliver();
}
function mapHarnessCompletionStatus(
status: AgentHarnessCompletionStatus,
): AgentInternalEventStatus {
if (status === "succeeded") {
return "ok";
}
return "error";
}
/** Returns true when completion delivery reached a persistent direct or steered path. */
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");
}
}
|