merge: integrate langgraph execution runtime

# Conflicts:
#	agent-run-gateway.test.mjs
#	capabilities.mjs
#	package.json
#	server.mjs
This commit is contained in:
john
2026-07-25 00:09:15 +08:00
88 changed files with 14319 additions and 90 deletions
+200 -8
View File
@@ -187,6 +187,24 @@ function positiveInteger(value, fallback) {
return Math.floor(n);
}
export function normalizeAgentRunWorkerIdentity(identity = {}) {
const runtimeRoot = path.resolve(
String(identity.runtimeRoot ?? process.cwd()).trim() || process.cwd(),
);
const buildId =
String(
identity.buildId
?? process.env.MEMIND_RUNTIME_BUILD_ID
?? process.env.MEMIND_RELEASE_ID
?? process.env.GIT_COMMIT
?? 'dev',
).trim() || 'dev';
const workerId =
String(identity.workerId ?? '').trim()
|| `${process.pid}:${crypto.randomUUID()}`;
return Object.freeze({ workerId, runtimeRoot, buildId });
}
function timeoutError(ms) {
const err = new Error(`agent run timed out after ${ms}ms`);
err.code = 'AGENT_RUN_TIMEOUT';
@@ -377,8 +395,10 @@ export function createAgentRunGateway({
conversationMemoryService = null,
syncUserPagesOnSuccess = null,
observePersonalMemoryOnSuccess = null,
observeWorkflowRun = null,
isSessionExternallyBusy = null,
validateRunDeliverables = null,
quiesceSessionOnTerminal = null,
retryDelaysMs = DEFAULT_RUN_RETRY_DELAYS_MS,
autoDispatch = envFlag(process.env.MEMIND_AGENT_RUN_AUTODISPATCH, true),
maxConcurrentRuns = positiveInteger(
@@ -393,7 +413,9 @@ export function createAgentRunGateway({
process.env.MEMIND_AGENT_RUN_HEARTBEAT_MS,
DEFAULT_RUN_HEARTBEAT_MS,
),
workerIdentity = null,
}) {
const worker = normalizeAgentRunWorkerIdentity(workerIdentity ?? {});
const sessionStore = resolveSessionAccess({ userAuth, sessionAccess });
const inFlight = new Set();
const queuedDispatches = [];
@@ -426,6 +448,105 @@ export function createAgentRunGateway({
);
}
async function quiesceTerminalSession(runId, {
userId,
sessionId,
requestId = null,
status,
}) {
if (!sessionId || typeof quiesceSessionOnTerminal !== 'function') return;
try {
const result = await quiesceSessionOnTerminal({
runId,
userId,
sessionId,
requestId,
status,
});
await appendEvent(runId, 'session_extensions_quiesced', {
sessionId,
status,
removed: result?.removed ?? [],
skipped: Boolean(result?.skipped),
cancellation: result?.cancellation ?? null,
});
} catch (err) {
await appendEvent(runId, 'session_extension_quiesce_failed', {
sessionId,
status,
error: err instanceof Error ? err.message : String(err),
}).catch(() => {});
console.warn(
`[AgentRun] failed to quiesce session ${sessionId}:`,
err instanceof Error ? err.message : err,
);
}
}
async function appendExecutionPlanEvent(input, source) {
const executionPlan = source?.executionPlan;
if (!executionPlan?.dryRun) return;
await appendEvent(input.runId, 'workflow_execution_planned', {
version: executionPlan.version ?? 'workflow-execution-decision-v1',
mode: source.mode ?? null,
candidateEngine: executionPlan.candidateEngine ?? null,
effectiveEngine: executionPlan.effectiveEngine ?? 'native',
fallbackEngine: executionPlan.fallbackEngine ?? 'native',
reason: executionPlan.reason ?? 'execution_gate_disabled',
candidateReason: executionPlan.candidateReason ?? null,
configVersion: executionPlan.configVersion ?? null,
bucket: executionPlan.bucket ?? null,
taskType: executionPlan.taskType ?? input.taskType ?? null,
dryRun: true,
handoffAllowed: false,
}).catch((error) => {
console.warn(
'[AgentRun] workflow execution plan event skipped:',
error instanceof Error ? error.message : error,
);
});
}
function dispatchShadowObservation(input) {
if (typeof observeWorkflowRun !== 'function' || input.toolMode !== 'code') return;
setImmediate(() => {
const observationStartedAt = nowMs();
void Promise.resolve()
.then(() => observeWorkflowRun(input))
.then(async (result) => {
await appendExecutionPlanEvent(input, result);
if (!result?.observed) return;
await appendEvent(input.runId, 'workflow_shadow_completed', {
engine: result.engine ?? 'langgraph',
mode: result.mode ?? 'shadow',
configVersion: result.configVersion ?? null,
status: result.shadowRun?.status ?? null,
phase: result.shadowRun?.phase ?? null,
taskType: result.shadowRun?.plan?.taskType ?? input.taskType ?? null,
executorAdapter: result.shadowRun?.plan?.executorAdapter ?? null,
latencyMs: Math.max(0, nowMs() - observationStartedAt),
});
})
.catch(async (error) => {
await appendExecutionPlanEvent(input, error);
console.warn(
'[AgentRun] workflow shadow observation failed:',
error instanceof Error ? error.message : error,
);
await appendEvent(input.runId, 'workflow_shadow_failed', {
code: String(error?.code ?? 'WORKFLOW_SHADOW_FAILED').slice(0, 128),
message: String(error instanceof Error ? error.message : error).slice(0, 1000),
latencyMs: Math.max(0, nowMs() - observationStartedAt),
}).catch((appendError) => {
console.warn(
'[AgentRun] workflow shadow failure event skipped:',
appendError instanceof Error ? appendError.message : appendError,
);
});
});
});
}
async function appendRunSnapshot(runId) {
if (!isRunStreamReplayEnabled()) return;
const row = await getRunById(runId);
@@ -528,8 +649,11 @@ export function createAgentRunGateway({
await pool.query(
`INSERT INTO h5_agent_runs
(id, user_id, agent_session_id, request_id, status, attempts,
user_message_json, error_message, created_at, updated_at, started_at, completed_at)
VALUES (?, ?, ?, ?, 'queued', 0, ?, NULL, ?, ?, NULL, NULL)`,
user_message_json, error_message, created_at, updated_at, started_at, completed_at,
required_runtime_root, required_build_id,
claimed_worker_id, claimed_runtime_root, claimed_build_id)
VALUES (?, ?, ?, ?, 'queued', 0, ?, NULL, ?, ?, NULL, NULL,
?, ?, NULL, NULL, NULL)`,
[
runId,
userId,
@@ -538,6 +662,8 @@ export function createAgentRunGateway({
serializeMessage(runMessage),
createdAt,
createdAt,
worker.runtimeRoot,
worker.buildId,
],
);
await appendEvent(runId, 'queued', {
@@ -545,9 +671,22 @@ export function createAgentRunGateway({
toolMode: normalizedToolMode,
taskType: normalizedTaskType,
forceDeepReasoning: Boolean(forceDeepReasoning),
requiredRuntimeRoot: worker.runtimeRoot,
requiredBuildId: worker.buildId,
});
const createdRun = projectRun(await getRunById(runId));
dispatchShadowObservation({
runId,
requestId: normalizedRequestId,
userId,
sessionId: sessionId || null,
workflowName: 'code-run-v1',
userMessage: runMessage,
toolMode: normalizedToolMode,
taskType: normalizedTaskType,
});
if (autoDispatch) dispatchRun(runId);
return projectRun(await getRunById(runId));
return createdRun;
}
async function prepareRunDeliverables({ userId, sessionId, runId, runStartedAtMs = null }) {
@@ -623,6 +762,9 @@ export function createAgentRunGateway({
attempt,
pid: process.pid,
heartbeatMs,
workerId: worker.workerId,
runtimeRoot: worker.runtimeRoot,
buildId: worker.buildId,
});
} catch (err) {
console.error('[AgentRun] worker heartbeat failed:', err instanceof Error ? err.message : err);
@@ -1188,12 +1330,32 @@ export function createAgentRunGateway({
const nextAttempt = Number(row.attempts ?? 0) + 1;
const [claim] = await pool.query(
`UPDATE h5_agent_runs
SET status = 'running', attempts = ?, started_at = COALESCE(started_at, ?), updated_at = ?, error_message = NULL
WHERE id = ? AND status IN ('queued', 'retryable')`,
[nextAttempt, nowMs(), nowMs(), runId],
SET status = 'running', attempts = ?, started_at = COALESCE(started_at, ?),
updated_at = ?, error_message = NULL,
claimed_worker_id = ?, claimed_runtime_root = ?, claimed_build_id = ?
WHERE id = ?
AND status IN ('queued', 'retryable')
AND (required_runtime_root IS NULL OR required_runtime_root = ?)
AND (required_build_id IS NULL OR required_build_id = ?)`,
[
nextAttempt,
nowMs(),
nowMs(),
worker.workerId,
worker.runtimeRoot,
worker.buildId,
runId,
worker.runtimeRoot,
worker.buildId,
],
);
if (Number(claim?.affectedRows ?? 0) === 0) return;
await appendEvent(runId, 'running', { attempt: nextAttempt });
await appendEvent(runId, 'running', {
attempt: nextAttempt,
workerId: worker.workerId,
runtimeRoot: worker.runtimeRoot,
buildId: worker.buildId,
});
const stopHeartbeat = startRunHeartbeat(runId, { attempt: nextAttempt });
try {
@@ -1221,12 +1383,28 @@ export function createAgentRunGateway({
await markRun(runId, retryable ? 'retryable' : 'failed', {
error_message: message,
completed_at: retryable ? null : nowMs(),
...(retryable
? {
claimed_worker_id: null,
claimed_runtime_root: null,
claimed_build_id: null,
}
: {}),
}, { expectedStatus: 'running' });
if (retryable && autoDispatch) {
setTimeout(() => dispatchRun(runId), retryDelaysMs[nextAttempt - 1]);
}
} finally {
stopHeartbeat();
const terminalRun = await getRunById(runId).catch(() => null);
if (terminalRun && TERMINAL_STATUSES.has(terminalRun.status)) {
await quiesceTerminalSession(runId, {
userId: terminalRun.user_id ?? row.user_id,
sessionId: terminalRun.agent_session_id ?? row.agent_session_id ?? null,
requestId: terminalRun.request_id ?? row.request_id ?? null,
status: terminalRun.status,
});
}
}
}
@@ -1439,6 +1617,12 @@ export function createAgentRunGateway({
if (!marked) continue;
item.status = 'succeeded';
item.recoveredAs = 'deliverables';
await quiesceTerminalSession(row.id, {
userId: row.user_id,
sessionId: row.agent_session_id ?? null,
requestId: row.request_id ?? null,
status: 'succeeded',
});
recovered.push(item);
continue;
}
@@ -1486,6 +1670,12 @@ export function createAgentRunGateway({
error: message,
});
item.status = 'failed';
await quiesceTerminalSession(row.id, {
userId: row.user_id,
sessionId: row.agent_session_id ?? null,
requestId: row.request_id ?? null,
status: 'failed',
});
}
recovered.push(item);
}
@@ -1522,9 +1712,11 @@ export function createAgentRunGateway({
`SELECT id
FROM h5_agent_runs
WHERE status IN ('queued', 'retryable')
AND (required_runtime_root IS NULL OR required_runtime_root = ?)
AND (required_build_id IS NULL OR required_build_id = ?)
ORDER BY updated_at ASC
LIMIT ?`,
[take],
[worker.runtimeRoot, worker.buildId, take],
);
for (const row of rows) {
dispatchRun(row.id);