feat: add shadow LangGraph orchestrator runtime
This commit is contained in:
+1
-1
@@ -252,7 +252,7 @@ export function createAdminApi({
|
||||
if (!orchestratorConfigService?.getRuntimeState) {
|
||||
return res.status(503).json({ message: 'Orchestrator 配置服务未启用' });
|
||||
}
|
||||
return res.json(await orchestratorConfigService.getRuntimeState());
|
||||
return res.json(await orchestratorConfigService.getRuntimeState({ probe: true }));
|
||||
});
|
||||
|
||||
adminApi.get('/skill-runtime/config', requireAdmin, async (_req, res) => {
|
||||
|
||||
+10
-3
@@ -129,8 +129,13 @@ test('admin orchestrator routes expose a versioned plug-in control plane', async
|
||||
updates.push({ config, context });
|
||||
return { ...state, config: { ...state.config, ...config }, configVersion: 2 };
|
||||
},
|
||||
async getRuntimeState() {
|
||||
return { ...state, source: 'admin-db' };
|
||||
async getRuntimeState(options) {
|
||||
assert.deepEqual(options, { probe: true });
|
||||
return {
|
||||
...state,
|
||||
source: 'admin-db',
|
||||
serviceHealth: { ok: true, status: 'healthy' },
|
||||
};
|
||||
},
|
||||
},
|
||||
plazaPosts: null,
|
||||
@@ -166,7 +171,9 @@ test('admin orchestrator routes expose a versioned plug-in control plane', async
|
||||
headers: { cookie: 'h5_user_session=token-admin' },
|
||||
});
|
||||
assert.equal(runtimeResponse.status, 200);
|
||||
assert.equal((await runtimeResponse.json()).source, 'admin-db');
|
||||
const runtimeBody = await runtimeResponse.json();
|
||||
assert.equal(runtimeBody.source, 'admin-db');
|
||||
assert.equal(runtimeBody.serviceHealth.status, 'healthy');
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
|
||||
+46
-1
@@ -289,6 +289,7 @@ export function createAgentRunGateway({
|
||||
conversationMemoryService = null,
|
||||
syncUserPagesOnSuccess = null,
|
||||
observePersonalMemoryOnSuccess = null,
|
||||
observeWorkflowRun = null,
|
||||
isSessionExternallyBusy = null,
|
||||
validateRunDeliverables = null,
|
||||
retryDelaysMs = DEFAULT_RUN_RETRY_DELAYS_MS,
|
||||
@@ -338,6 +339,39 @@ export function createAgentRunGateway({
|
||||
);
|
||||
}
|
||||
|
||||
function dispatchShadowObservation(input) {
|
||||
if (typeof observeWorkflowRun !== 'function' || input.toolMode !== 'code') return;
|
||||
setImmediate(() => {
|
||||
void Promise.resolve()
|
||||
.then(() => observeWorkflowRun(input))
|
||||
.then(async (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,
|
||||
});
|
||||
})
|
||||
.catch(async (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),
|
||||
}).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);
|
||||
@@ -443,8 +477,19 @@ export function createAgentRunGateway({
|
||||
taskType: normalizedTaskType,
|
||||
forceDeepReasoning: Boolean(forceDeepReasoning),
|
||||
});
|
||||
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 }) {
|
||||
|
||||
@@ -374,6 +374,98 @@ test('agent run creation stores code tool metadata in the queued message', async
|
||||
});
|
||||
});
|
||||
|
||||
test('code run shadow observation is fire-and-forget and records completion separately', async () => {
|
||||
const pool = createFakePool();
|
||||
let releaseObservation;
|
||||
let observedInput = null;
|
||||
const observation = new Promise((resolve) => {
|
||||
releaseObservation = resolve;
|
||||
});
|
||||
const gateway = createAgentRunGateway({
|
||||
pool,
|
||||
userAuth: {},
|
||||
tkmindProxy: {},
|
||||
autoDispatch: false,
|
||||
observeWorkflowRun: async (input) => {
|
||||
observedInput = input;
|
||||
return observation;
|
||||
},
|
||||
});
|
||||
|
||||
const run = await gateway.createRun('user-shadow', {
|
||||
requestId: 'req-shadow',
|
||||
sessionId: 'session-shadow',
|
||||
userMessage: { role: 'user', content: [{ type: 'text', text: 'refactor this' }] },
|
||||
toolMode: 'code',
|
||||
taskType: 'repo_refactor',
|
||||
});
|
||||
|
||||
assert.equal(run.status, 'queued');
|
||||
await waitFor(() => observedInput != null);
|
||||
assert.equal(observedInput.runId, run.id);
|
||||
assert.equal(observedInput.workflowName, 'code-run-v1');
|
||||
assert.equal(
|
||||
pool.events.some((event) => event.eventType === 'workflow_shadow_completed'),
|
||||
false,
|
||||
);
|
||||
|
||||
releaseObservation({
|
||||
observed: true,
|
||||
engine: 'langgraph',
|
||||
mode: 'shadow',
|
||||
configVersion: 3,
|
||||
shadowRun: { status: 'succeeded', phase: 'completed' },
|
||||
});
|
||||
await waitFor(() => pool.events.some(
|
||||
(event) => event.eventType === 'workflow_shadow_completed',
|
||||
));
|
||||
const event = pool.events.find((item) => item.eventType === 'workflow_shadow_completed');
|
||||
assert.deepEqual(JSON.parse(event.dataJson), {
|
||||
engine: 'langgraph',
|
||||
mode: 'shadow',
|
||||
configVersion: 3,
|
||||
status: 'succeeded',
|
||||
phase: 'completed',
|
||||
});
|
||||
});
|
||||
|
||||
test('shadow observer failure cannot fail a Native run and chat runs are not observed', async () => {
|
||||
const pool = createFakePool();
|
||||
let observations = 0;
|
||||
const gateway = createAgentRunGateway({
|
||||
pool,
|
||||
userAuth: {},
|
||||
tkmindProxy: {},
|
||||
autoDispatch: false,
|
||||
observeWorkflowRun: async () => {
|
||||
observations += 1;
|
||||
const error = new Error('shadow unavailable');
|
||||
error.code = 'SHADOW_UNAVAILABLE';
|
||||
throw error;
|
||||
},
|
||||
});
|
||||
|
||||
const codeRun = await gateway.createRun('user-shadow', {
|
||||
requestId: 'req-shadow-failure',
|
||||
userMessage: { role: 'user', content: 'change code' },
|
||||
toolMode: 'code',
|
||||
});
|
||||
const chatRun = await gateway.createRun('user-shadow', {
|
||||
requestId: 'req-chat-no-shadow',
|
||||
userMessage: { role: 'user', content: 'hello' },
|
||||
toolMode: 'chat',
|
||||
});
|
||||
|
||||
assert.equal(codeRun.status, 'queued');
|
||||
assert.equal(chatRun.status, 'queued');
|
||||
await waitFor(() => pool.events.some(
|
||||
(event) => event.eventType === 'workflow_shadow_failed',
|
||||
));
|
||||
assert.equal(observations, 1);
|
||||
const failure = pool.events.find((event) => event.eventType === 'workflow_shadow_failed');
|
||||
assert.equal(JSON.parse(failure.dataJson).code, 'SHADOW_UNAVAILABLE');
|
||||
});
|
||||
|
||||
test('agent run starts a session and marks submitted reply as succeeded', async () => {
|
||||
const pool = createFakePool();
|
||||
const submitted = [];
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
ORCHESTRATOR_POSTGRES_PASSWORD=replace-with-a-long-random-password
|
||||
MEMIND_ORCHESTRATOR_SERVICE_TOKEN=replace-with-a-long-random-service-token
|
||||
MEMIND_ORCHESTRATOR_PORT=8093
|
||||
@@ -0,0 +1,46 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: memind_orchestrator
|
||||
POSTGRES_USER: memind_orchestrator
|
||||
POSTGRES_PASSWORD: ${ORCHESTRATOR_POSTGRES_PASSWORD:?set ORCHESTRATOR_POSTGRES_PASSWORD}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U memind_orchestrator -d memind_orchestrator"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
volumes:
|
||||
- orchestrator_postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- orchestrator_internal
|
||||
|
||||
orchestrator:
|
||||
build:
|
||||
context: ../../services/orchestrator
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
MEMIND_ORCHESTRATOR_HOST: 0.0.0.0
|
||||
MEMIND_ORCHESTRATOR_PORT: 8093
|
||||
MEMIND_ORCHESTRATOR_CHECKPOINT_MODE: postgres
|
||||
MEMIND_ORCHESTRATOR_DATABASE_URL: postgresql://memind_orchestrator:${ORCHESTRATOR_POSTGRES_PASSWORD}@postgres:5432/memind_orchestrator
|
||||
MEMIND_ORCHESTRATOR_DATABASE_SCHEMA: memind_orchestrator
|
||||
MEMIND_ORCHESTRATOR_SERVICE_TOKEN: ${MEMIND_ORCHESTRATOR_SERVICE_TOKEN:?set MEMIND_ORCHESTRATOR_SERVICE_TOKEN}
|
||||
ports:
|
||||
- "127.0.0.1:${MEMIND_ORCHESTRATOR_PORT:-8093}:8093"
|
||||
networks:
|
||||
- orchestrator_internal
|
||||
- orchestrator_edge
|
||||
|
||||
networks:
|
||||
orchestrator_internal:
|
||||
internal: true
|
||||
orchestrator_edge:
|
||||
|
||||
volumes:
|
||||
orchestrator_postgres_data:
|
||||
@@ -54,6 +54,11 @@ Native selection.
|
||||
The initial workflow allowlist contains only `code-run-v1`. Ordinary chat and
|
||||
existing Goosed session traffic remain outside the Orchestrator path.
|
||||
|
||||
Phase 2 implements only the `off` and `shadow` execution semantics. Although
|
||||
Canary and Active can be configured and evaluated by the control plane, the
|
||||
Portal does not hand execution ownership to LangGraph yet. This prevents an
|
||||
administrative configuration mistake from creating two task executors.
|
||||
|
||||
## Protocol
|
||||
|
||||
The framework-neutral contracts are:
|
||||
@@ -61,7 +66,7 @@ The framework-neutral contracts are:
|
||||
- `orchestrator-run-v1`
|
||||
- `orchestrator-event-v1`
|
||||
|
||||
The eventual internal API is:
|
||||
The implemented internal API is:
|
||||
|
||||
```text
|
||||
POST /v1/runs
|
||||
@@ -75,13 +80,38 @@ External state changes must use an idempotency key derived from run, graph
|
||||
version, node, and attempt. Graph state stores resource references and decisions,
|
||||
not API keys, large logs, binary artifacts, or absolute production paths.
|
||||
|
||||
The Phase 2 graph has three deterministic nodes:
|
||||
|
||||
```text
|
||||
validate_run -> build_plan -> finalize_run
|
||||
```
|
||||
|
||||
It requires `policy.executionMode=observe-only` and
|
||||
`policy.sideEffectsAllowed=false`. It projects the Native executor boundary but
|
||||
cannot call Goosed, Aider, OpenHands, Tool Gateway, or the filesystem.
|
||||
|
||||
## Failure isolation
|
||||
|
||||
Portal invokes Shadow only after the product run and its `queued` event have
|
||||
been committed. The observation is scheduled without awaiting it. A timeout or
|
||||
failure:
|
||||
|
||||
1. cannot reject or delay `createRun`;
|
||||
2. cannot mutate the Native run status;
|
||||
3. is projected as `workflow_shadow_failed`;
|
||||
4. remains removable by deleting the observer wiring and changing only the
|
||||
service URL boundary.
|
||||
|
||||
Successful observations are projected as `workflow_shadow_completed`; graph
|
||||
checkpoints stay in the Orchestrator-owned PostgreSQL database.
|
||||
|
||||
## Deployment evolution
|
||||
|
||||
1. Local native Orchestrator process with a development checkpoint database.
|
||||
2. 103 shadow LaunchAgent; no production task claim.
|
||||
3. User-allowlisted code-run canary with Native rollback.
|
||||
4. Isolated Aider/OpenHands executor workers.
|
||||
5. Containerized Orchestrator in its own Compose project.
|
||||
1. Local native Orchestrator process with an explicit MemorySaver for debugging.
|
||||
2. Colima Compose with an isolated PostgreSQL checkpoint database.
|
||||
3. Approved server-side shadow service; no production task claim.
|
||||
4. User-allowlisted code-run canary with Native rollback.
|
||||
5. Isolated Aider/OpenHands executor workers.
|
||||
6. Move the same service contract to Linux or Kubernetes when horizontal scaling
|
||||
or independent ownership becomes necessary.
|
||||
|
||||
|
||||
@@ -240,6 +240,23 @@ export type OrchestratorConfigState = {
|
||||
engines: OrchestratorEngineDescriptor[];
|
||||
};
|
||||
|
||||
export type OrchestratorServiceHealth = {
|
||||
checkedAt: number;
|
||||
ok: boolean;
|
||||
status: 'healthy' | 'unhealthy' | 'unconfigured' | 'timeout' | 'unreachable' | 'fetch_unavailable';
|
||||
latencyMs: number;
|
||||
httpStatus: number | null;
|
||||
details: {
|
||||
service: string | null;
|
||||
checkpoint: { kind?: string; durable?: boolean } | null;
|
||||
execution: string | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type OrchestratorRuntimeState = OrchestratorConfigState & {
|
||||
serviceHealth: OrchestratorServiceHealth;
|
||||
};
|
||||
|
||||
// ─── Summary ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function fetchAdminSummary() {
|
||||
@@ -272,6 +289,10 @@ export async function updateOrchestratorConfig(config: OrchestratorConfig) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchOrchestratorRuntime() {
|
||||
return adminFetch<OrchestratorRuntimeState>('/admin-api/orchestrator/runtime');
|
||||
}
|
||||
|
||||
// ─── Users ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function fetchAdminUsers(params: {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
fetchOrchestratorConfig,
|
||||
fetchOrchestratorRuntime,
|
||||
updateOrchestratorConfig,
|
||||
type OrchestratorConfig,
|
||||
type OrchestratorConfigState,
|
||||
type OrchestratorServiceHealth,
|
||||
} from '../../api/admin';
|
||||
|
||||
function listToText(values: string[]) {
|
||||
@@ -31,6 +33,8 @@ export function OrchestratorPage() {
|
||||
const [workflowAllowlist, setWorkflowAllowlist] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [checking, setChecking] = useState(false);
|
||||
const [serviceHealth, setServiceHealth] = useState<OrchestratorServiceHealth | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
|
||||
@@ -54,6 +58,19 @@ export function OrchestratorPage() {
|
||||
void load();
|
||||
}, []);
|
||||
|
||||
const checkService = async () => {
|
||||
setChecking(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await fetchOrchestratorRuntime();
|
||||
setServiceHealth(result.serviceHealth);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '服务检查失败');
|
||||
} finally {
|
||||
setChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
const normalizedDraft = useMemo(() => draft ? {
|
||||
...draft,
|
||||
userAllowlist: textToList(userAllowlist),
|
||||
@@ -75,7 +92,7 @@ export function OrchestratorPage() {
|
||||
}
|
||||
if (
|
||||
normalizedDraft.mode === 'active'
|
||||
&& !window.confirm('确认启用 Active?工作流白名单内的任务将由 LangGraph Orchestrator 接管。')
|
||||
&& !window.confirm('确认保存 Active 预配置?Phase 2 不会交出执行权,Native 仍是唯一执行者。')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -104,7 +121,7 @@ export function OrchestratorPage() {
|
||||
: state.runtime.shadowsLangGraph
|
||||
? 'Shadow 已生效:Native 继续执行,LangGraph 只观察和生成决策。'
|
||||
: state.runtime.executesLangGraph
|
||||
? 'LangGraph 路由已生效。'
|
||||
? 'Canary / Active 路由决策已预配置;Phase 2 尚未交出执行权,Native 仍是唯一执行者。'
|
||||
: '配置有效。';
|
||||
|
||||
return (
|
||||
@@ -158,8 +175,8 @@ export function OrchestratorPage() {
|
||||
>
|
||||
<option value="off">Off — Native only</option>
|
||||
<option value="shadow">Shadow — Native 执行,LangGraph 观察</option>
|
||||
<option value="canary">Canary — 白名单/比例灰度</option>
|
||||
<option value="active">Active — 白名单工作流正式接管</option>
|
||||
<option value="canary">Canary — 预配置,Phase 2 不接管</option>
|
||||
<option value="active">Active — 预配置,Phase 2 不接管</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
@@ -195,6 +212,25 @@ export function OrchestratorPage() {
|
||||
|
||||
<div className="card grid">
|
||||
<h3 style={{ margin: 0 }}>服务连接</h3>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
|
||||
<span>
|
||||
<strong>实时健康状态:</strong>
|
||||
{serviceHealth
|
||||
? `${serviceHealth.ok ? '健康' : '不可用'}(${serviceHealth.status},${serviceHealth.latencyMs} ms)`
|
||||
: '尚未检查'}
|
||||
{serviceHealth?.details?.checkpoint
|
||||
? ` · checkpoint ${serviceHealth.details.checkpoint.kind ?? 'unknown'}`
|
||||
: ''}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn secondary"
|
||||
onClick={() => void checkService()}
|
||||
disabled={checking}
|
||||
>
|
||||
{checking ? '检查中…' : '测试连接'}
|
||||
</button>
|
||||
</div>
|
||||
<label>
|
||||
<strong>Orchestrator URL</strong>
|
||||
<input
|
||||
@@ -244,7 +280,7 @@ export function OrchestratorPage() {
|
||||
<label>
|
||||
<strong>工作流白名单</strong>
|
||||
<span style={{ display: 'block', color: '#68716c', fontSize: 12, margin: '4px 0 8px' }}>
|
||||
每行一个。Active 也只接管这里列出的工作流。
|
||||
每行一个。未来启用 Active 时也只接管这里列出的工作流。
|
||||
</span>
|
||||
<textarea
|
||||
rows={8}
|
||||
|
||||
Generated
+284
-1
@@ -8,6 +8,9 @@
|
||||
"name": "tkmind-h5",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@langchain/core": "^1.2.3",
|
||||
"@langchain/langgraph": "^1.4.8",
|
||||
"@langchain/langgraph-checkpoint-postgres": "^1.0.4",
|
||||
"@node-rs/argon2": "^2.0.2",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"debug": "^4.4.3",
|
||||
@@ -320,6 +323,12 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cfworker/json-schema": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz",
|
||||
"integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
|
||||
@@ -1432,6 +1441,144 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/core": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.2.3.tgz",
|
||||
"integrity": "sha512-F+L5SsciykwDl7eDxacnhDTcWe1IF6jetzfkvI5PPfq6ogWHO7xcjU90SGh/3lqbbS0tgun+qF01KIqxawrCsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cfworker/json-schema": "^4.0.2",
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"js-tiktoken": "^1.0.12",
|
||||
"langsmith": ">=0.5.0 <1.0.0",
|
||||
"mustache": "^4.2.0",
|
||||
"p-queue": "^6.6.2",
|
||||
"zod": "^3.25.76 || ^4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph": {
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.4.8.tgz",
|
||||
"integrity": "sha512-DN1Np1XefdBEbp1qBKlt39cwoL743AAGpR5Ipja0gY2YbWvsoQnOTIrjnj/orSAhaUYsdTKS8VSWdFzsHZo6Ig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@langchain/langgraph-checkpoint": "^1.1.3",
|
||||
"@langchain/langgraph-sdk": "~1.9.26",
|
||||
"@langchain/protocol": "^0.0.18",
|
||||
"@standard-schema/spec": "1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": "^1.1.48",
|
||||
"zod": "^3.25.32 || ^4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-checkpoint": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.1.3.tgz",
|
||||
"integrity": "sha512-wgzdQNeEsdw1e+4lvlj0tdq/RYR/k1vPin10g0ymGoehZDDgd9nvIllGXSXN4TFgF9sf5qQP/KTkOcLfeseIhA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": "^1.1.48"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-checkpoint-postgres": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint-postgres/-/langgraph-checkpoint-postgres-1.0.4.tgz",
|
||||
"integrity": "sha512-wuCPq7VVqwi0+nQC5GOmheoXGJHAybgXbkA9Nn0J8nYW61WFJ3uqb4wyMbLFhAlya99EaxNNsi3FzdzV2COSzw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg": "^8.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": "^1.1.44",
|
||||
"@langchain/langgraph-checkpoint": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-sdk": {
|
||||
"version": "1.9.28",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.9.28.tgz",
|
||||
"integrity": "sha512-4j3XuM0PvtmAbL8mPfBS99ez3+ytRfgbOpAR/nOeaejTRF3Q9dNw2QnaGLGng8wLPtGLoSj+SYgUOVxy9Bv9vg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@langchain/protocol": "^0.0.18",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"p-queue": "^9.0.1",
|
||||
"p-retry": "^7.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": "^1.1.48",
|
||||
"react": "^18 || ^19",
|
||||
"react-dom": "^18 || ^19",
|
||||
"svelte": "^4.0.0 || ^5.0.0",
|
||||
"vue": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"svelte": {
|
||||
"optional": true
|
||||
},
|
||||
"vue": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-sdk/node_modules/eventemitter3": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||||
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@langchain/langgraph-sdk/node_modules/p-queue": {
|
||||
"version": "9.3.3",
|
||||
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.3.tgz",
|
||||
"integrity": "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^5.0.4",
|
||||
"p-timeout": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-sdk/node_modules/p-timeout": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz",
|
||||
"integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/protocol": {
|
||||
"version": "0.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/protocol/-/protocol-0.0.18.tgz",
|
||||
"integrity": "sha512-XW1egQtPfsGI41w2AMZNFZrUIwFSQHTjVMZs0OaTpCAvht/QLoaPN8FQcsysMVypOhupG28J29yOorrc70otBQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||
@@ -2330,6 +2477,12 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
||||
@@ -2401,6 +2554,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz",
|
||||
@@ -3902,6 +4061,18 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-network-error": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz",
|
||||
"integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
@@ -3932,6 +4103,15 @@
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-tiktoken": {
|
||||
"version": "1.0.21",
|
||||
"resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz",
|
||||
"integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -4016,6 +4196,39 @@
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/langsmith": {
|
||||
"version": "0.8.6",
|
||||
"resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.8.6.tgz",
|
||||
"integrity": "sha512-tl7cV79S/SkJNskCo7joRIVCYy+ljVLs4L1hgC2qT01x4CAWX+6IJzmfXGYAJcn1ecpaQr63WRjkLO9jefmOmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-queue": "6.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "*",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "*",
|
||||
"@opentelemetry/sdk-trace-base": "*",
|
||||
"openai": "*",
|
||||
"ws": ">=7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/exporter-trace-otlp-proto": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/sdk-trace-base": {
|
||||
"optional": true
|
||||
},
|
||||
"openai": {
|
||||
"optional": true
|
||||
},
|
||||
"ws": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/lazystream": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
|
||||
@@ -4340,6 +4553,15 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mustache": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
|
||||
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mustache": "bin/mustache"
|
||||
}
|
||||
},
|
||||
"node_modules/mysql2": {
|
||||
"version": "3.22.5",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.22.5.tgz",
|
||||
@@ -4470,6 +4692,15 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/p-finally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
"integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
@@ -4497,6 +4728,49 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-queue": {
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
|
||||
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^4.0.4",
|
||||
"p-timeout": "^3.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-retry": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.1.tgz",
|
||||
"integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-network-error": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-timeout": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
|
||||
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-finally": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
@@ -5833,7 +6107,7 @@
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -5948,6 +6222,15 @@
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||||
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -23,6 +23,7 @@
|
||||
"dev:vite": "vite",
|
||||
"dev:server": "node server.mjs",
|
||||
"dev:adm": "node admin-server.mjs",
|
||||
"dev:orchestrator": "node services/orchestrator/server.mjs",
|
||||
"dev:deep-search": "node deep-search-server.mjs",
|
||||
"dev:plaza-express": "node plaza-server.mjs",
|
||||
"dev:ops": "cd ops && npm run dev",
|
||||
@@ -61,7 +62,7 @@
|
||||
"test:scenario": "node scripts/run-scenario-test.mjs",
|
||||
"verify:children-hobby-diet-survey": "node scripts/verify-children-hobby-diet-survey.mjs",
|
||||
"test:scenario:john4-diet": "node scripts/run-scenario-test.mjs --scenario john4-children-hobby-diet-update",
|
||||
"test": "node --test auth.test.mjs asr-proxy.test.mjs billing.test.mjs billing-token-state.test.mjs billing-recharge.test.mjs wechat-pay.test.mjs wechat-oauth.test.mjs wechat-mp.test.mjs wechat-media.test.mjs wechat/image-generation-policy.test.mjs wechat/verify/generated-thumbnail.test.mjs schedule-intent.test.mjs schedule-reminder-worker.test.mjs capabilities.test.mjs policies.test.mjs chat-skills.test.mjs chat-intent-router.test.mjs chat-finish-sync.test.mjs chat-agent-run-gate.test.mjs conversation-display.test.mjs user-publish.test.mjs user-memory-profile.test.mjs skills-registry.test.mjs skill-runtime-policy.test.mjs excel-analyst.test.mjs agent-run-gateway.test.mjs agent-run-routes.test.mjs session-broker.test.mjs sse-event-taxonomy.test.mjs goosed-proxy-boundary.test.mjs agent-run-stream.test.mjs mindspace-h5-html-finish-guard.test.mjs admin-routes.test.mjs services/orchestrator/admin-config.test.mjs services/orchestrator/contracts.test.mjs image-make-admin-config.test.mjs asset-gateway.test.mjs image-make-client.test.mjs mindspace-image-generation.test.mjs mindspace-image-generation-routes.test.mjs mindspace-image-review.test.mjs mindspace-run-public-html-scope.test.mjs direct-chat-service.test.mjs tool-gateway.test.mjs mindspace.test.mjs mindspace-scan.test.mjs mindspace-assets.test.mjs mindspace-local-runtime-services.test.mjs mindspace-local-server-adapter.test.mjs mindspace-public-asset-token.test.mjs mindspace-remote-server-adapter.test.mjs mindspace-server-adapter.test.mjs mindspace-pages.test.mjs mindspace-page-sync-service.test.mjs public-site-bases.test.mjs mindspace-html-download-links.test.mjs mindspace-page-purge.test.mjs mindspace-public-delivery.test.mjs mindspace-public-page-context.test.mjs mindspace-published-page-csp.test.mjs mindspace-published-script-localize.test.mjs agent-run-deliverable-check.test.mjs mindspace-public-route.test.mjs mindspace-publications.test.mjs mindspace-public-links.test.mjs mindspace-chat-save.test.mjs mindspace-chat-docx-package.test.mjs mindspace-public-finish-sync.test.mjs mindspace-chat-context.test.mjs mindspace-canonical-url.test.mjs mindspace-conversation-package.test.mjs mindspace-conversation-package-backfill.test.mjs mindspace-conversation-package-public-html.test.mjs mindspace-conversation-package-verify.test.mjs mindspace-conversation-package-registry.test.mjs mindspace-conversation-package-routes.test.mjs mindspace-conversation-package-store.test.mjs mindspace-conversation-schema.test.mjs mindspace-runtime-config.test.mjs mindspace-config.test.mjs mindspace-analytics.test.mjs mindspace-service.test.mjs mindspace-storage-adapter.test.mjs mindspace-content-scan.test.mjs mindspace-html-localize.test.mjs mindspace-visual-editor.test.mjs mindspace-cleanup.test.mjs mindspace-thumbnails.test.mjs mindspace-workspace-thumbnails.test.mjs mindspace-workspace-sync.test.mjs mindspace-asset-preview.test.mjs mindspace-agent-jobs.test.mjs mindspace-agent-runner.test.mjs mindspace-sandbox-mcp.test.mjs mindspace-userdata-postgres.test.mjs postgres-user-data-space-service.test.mjs user-data-space-service.test.mjs page-data-routes.test.mjs page-access-policy.test.mjs page-access-visitor.test.mjs page-data-public-service.test.mjs page-data-integration.test.mjs page-data-log-store.test.mjs page-data-ops.test.mjs page-data-session-store.test.mjs page-data-browser-client.test.mjs page-data-policy-index.test.mjs message-stream.test.mjs mindspace-service/mindspace-rpc-server.test.mjs plaza-posts.test.mjs plaza-interactions.test.mjs plaza-algorithm.test.mjs plaza-seo.test.mjs plaza-ops.test.mjs user-auth.test.mjs llm-providers.test.mjs admin-guard.test.mjs user-feedback.test.mjs memory-v2.test.mjs memory-v2-admin-config.test.mjs memory-v2-lifecycle.test.mjs memory-v2-adapter-scaffold.test.mjs memory-v2-backend-contract.test.mjs memory-v2-health.test.mjs memory-v2-runtime.test.mjs memory-v2-plugin-backends.test.mjs memory-v2-pgvector.test.mjs memory-v2-pgvector-schema.test.mjs memory-v2-pgvector-backfill.test.mjs memory-v2-pgvector-smoke.test.mjs memory-v2-qdrant.test.mjs memory-v2-weaviate.test.mjs memory-v2-mem0.test.mjs memory-v2-letta.test.mjs memory-v2-external-adapters.test.mjs scripts/embed-memory-v2-local-hash.test.mjs scripts/check-memory-v2-app-canary.test.mjs scripts/check-memory-v2-config-gaps.test.mjs scripts/check-memory-v2-contracts.test.mjs scripts/check-memory-v2-health.test.mjs scripts/check-memory-v2-session-flow.test.mjs scripts/check-memory-v2-stack.test.mjs scripts/setup-memory-v2-pgvector-schema.test.mjs scripts/backfill-memory-v2-pgvector.test.mjs scripts/scaffold-memory-v2-backend.test.mjs scripts/smoke-memory-v2-pgvector.test.mjs scripts/smoke-memory-v2-qdrant.test.mjs scripts/smoke-memory-v2-external.test.mjs scripts/mock-memory-v2-services.test.mjs",
|
||||
"test": "node --test auth.test.mjs asr-proxy.test.mjs billing.test.mjs billing-token-state.test.mjs billing-recharge.test.mjs wechat-pay.test.mjs wechat-oauth.test.mjs wechat-mp.test.mjs wechat-media.test.mjs wechat/image-generation-policy.test.mjs wechat/verify/generated-thumbnail.test.mjs schedule-intent.test.mjs schedule-reminder-worker.test.mjs capabilities.test.mjs policies.test.mjs chat-skills.test.mjs chat-intent-router.test.mjs chat-finish-sync.test.mjs chat-agent-run-gate.test.mjs conversation-display.test.mjs user-publish.test.mjs user-memory-profile.test.mjs skills-registry.test.mjs skill-runtime-policy.test.mjs excel-analyst.test.mjs agent-run-gateway.test.mjs agent-run-routes.test.mjs session-broker.test.mjs sse-event-taxonomy.test.mjs goosed-proxy-boundary.test.mjs agent-run-stream.test.mjs mindspace-h5-html-finish-guard.test.mjs admin-routes.test.mjs services/orchestrator/admin-config.test.mjs services/orchestrator/contracts.test.mjs services/orchestrator/checkpoint.test.mjs services/orchestrator/runtime.test.mjs services/orchestrator/app.test.mjs services/orchestrator/shadow-observer.test.mjs image-make-admin-config.test.mjs asset-gateway.test.mjs image-make-client.test.mjs mindspace-image-generation.test.mjs mindspace-image-generation-routes.test.mjs mindspace-image-review.test.mjs mindspace-run-public-html-scope.test.mjs direct-chat-service.test.mjs tool-gateway.test.mjs mindspace.test.mjs mindspace-scan.test.mjs mindspace-assets.test.mjs mindspace-local-runtime-services.test.mjs mindspace-local-server-adapter.test.mjs mindspace-public-asset-token.test.mjs mindspace-remote-server-adapter.test.mjs mindspace-server-adapter.test.mjs mindspace-pages.test.mjs mindspace-page-sync-service.test.mjs public-site-bases.test.mjs mindspace-html-download-links.test.mjs mindspace-page-purge.test.mjs mindspace-public-delivery.test.mjs mindspace-public-page-context.test.mjs mindspace-published-page-csp.test.mjs mindspace-published-script-localize.test.mjs agent-run-deliverable-check.test.mjs mindspace-public-route.test.mjs mindspace-publications.test.mjs mindspace-public-links.test.mjs mindspace-chat-save.test.mjs mindspace-chat-docx-package.test.mjs mindspace-public-finish-sync.test.mjs mindspace-chat-context.test.mjs mindspace-canonical-url.test.mjs mindspace-conversation-package.test.mjs mindspace-conversation-package-backfill.test.mjs mindspace-conversation-package-public-html.test.mjs mindspace-conversation-package-verify.test.mjs mindspace-conversation-package-registry.test.mjs mindspace-conversation-package-routes.test.mjs mindspace-conversation-package-store.test.mjs mindspace-conversation-schema.test.mjs mindspace-runtime-config.test.mjs mindspace-config.test.mjs mindspace-analytics.test.mjs mindspace-service.test.mjs mindspace-storage-adapter.test.mjs mindspace-content-scan.test.mjs mindspace-html-localize.test.mjs mindspace-visual-editor.test.mjs mindspace-cleanup.test.mjs mindspace-thumbnails.test.mjs mindspace-workspace-thumbnails.test.mjs mindspace-workspace-sync.test.mjs mindspace-asset-preview.test.mjs mindspace-agent-jobs.test.mjs mindspace-agent-runner.test.mjs mindspace-sandbox-mcp.test.mjs mindspace-userdata-postgres.test.mjs postgres-user-data-space-service.test.mjs user-data-space-service.test.mjs page-data-routes.test.mjs page-access-policy.test.mjs page-access-visitor.test.mjs page-data-public-service.test.mjs page-data-integration.test.mjs page-data-log-store.test.mjs page-data-ops.test.mjs page-data-session-store.test.mjs page-data-browser-client.test.mjs page-data-policy-index.test.mjs message-stream.test.mjs mindspace-service/mindspace-rpc-server.test.mjs plaza-posts.test.mjs plaza-interactions.test.mjs plaza-algorithm.test.mjs plaza-seo.test.mjs plaza-ops.test.mjs user-auth.test.mjs llm-providers.test.mjs admin-guard.test.mjs user-feedback.test.mjs memory-v2.test.mjs memory-v2-admin-config.test.mjs memory-v2-lifecycle.test.mjs memory-v2-adapter-scaffold.test.mjs memory-v2-backend-contract.test.mjs memory-v2-health.test.mjs memory-v2-runtime.test.mjs memory-v2-plugin-backends.test.mjs memory-v2-pgvector.test.mjs memory-v2-pgvector-schema.test.mjs memory-v2-pgvector-backfill.test.mjs memory-v2-pgvector-smoke.test.mjs memory-v2-qdrant.test.mjs memory-v2-weaviate.test.mjs memory-v2-mem0.test.mjs memory-v2-letta.test.mjs memory-v2-external-adapters.test.mjs scripts/embed-memory-v2-local-hash.test.mjs scripts/check-memory-v2-app-canary.test.mjs scripts/check-memory-v2-config-gaps.test.mjs scripts/check-memory-v2-contracts.test.mjs scripts/check-memory-v2-health.test.mjs scripts/check-memory-v2-session-flow.test.mjs scripts/check-memory-v2-stack.test.mjs scripts/setup-memory-v2-pgvector-schema.test.mjs scripts/backfill-memory-v2-pgvector.test.mjs scripts/scaffold-memory-v2-backend.test.mjs scripts/smoke-memory-v2-pgvector.test.mjs scripts/smoke-memory-v2-qdrant.test.mjs scripts/smoke-memory-v2-external.test.mjs scripts/mock-memory-v2-services.test.mjs",
|
||||
"test:episodic-memory": "node --test episodic-memory.test.mjs direct-chat-service.test.mjs chat-intent-router.test.mjs",
|
||||
"test:deep-search": "node --test deep-search.test.mjs mindsearch.test.mjs",
|
||||
"test:image-review": "node --test mindspace-image-review.test.mjs mindspace-image-generation.test.mjs",
|
||||
@@ -95,15 +96,18 @@
|
||||
"deploy:plaza-105": "bash scripts/deploy-plaza-105.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langchain/core": "^1.2.3",
|
||||
"@langchain/langgraph": "^1.4.8",
|
||||
"@langchain/langgraph-checkpoint-postgres": "^1.0.4",
|
||||
"@node-rs/argon2": "^2.0.2",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"debug": "^4.4.3",
|
||||
"exceljs": "^4.4.0",
|
||||
"express": "^4.21.2",
|
||||
"jszip": "^3.10.1",
|
||||
"framer-motion": "^12.42.0",
|
||||
"http-proxy-middleware": "^3.0.7",
|
||||
"jsonrepair": "^3.14.0",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^1.21.0",
|
||||
"mysql2": "^3.22.5",
|
||||
"pg": "^8.22.0",
|
||||
|
||||
Generated
+225
@@ -8,6 +8,15 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@langchain/core':
|
||||
specifier: ^1.2.3
|
||||
version: 1.2.3(ws@8.21.0)
|
||||
'@langchain/langgraph':
|
||||
specifier: ^1.4.8
|
||||
version: 1.4.8(@langchain/core@1.2.3(ws@8.21.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3)
|
||||
'@langchain/langgraph-checkpoint-postgres':
|
||||
specifier: ^1.0.4
|
||||
version: 1.0.4(@langchain/core@1.2.3(ws@8.21.0))(@langchain/langgraph-checkpoint@1.1.3(@langchain/core@1.2.3(ws@8.21.0)))
|
||||
'@node-rs/argon2':
|
||||
specifier: ^2.0.2
|
||||
version: 2.0.2
|
||||
@@ -173,6 +182,9 @@ packages:
|
||||
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@cfworker/json-schema@4.1.1':
|
||||
resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==}
|
||||
|
||||
'@emnapi/core@1.11.1':
|
||||
resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==}
|
||||
|
||||
@@ -522,6 +534,51 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@langchain/core@1.2.3':
|
||||
resolution: {integrity: sha512-F+L5SsciykwDl7eDxacnhDTcWe1IF6jetzfkvI5PPfq6ogWHO7xcjU90SGh/3lqbbS0tgun+qF01KIqxawrCsA==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@langchain/langgraph-checkpoint-postgres@1.0.4':
|
||||
resolution: {integrity: sha512-wuCPq7VVqwi0+nQC5GOmheoXGJHAybgXbkA9Nn0J8nYW61WFJ3uqb4wyMbLFhAlya99EaxNNsi3FzdzV2COSzw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@langchain/core': ^1.1.44
|
||||
'@langchain/langgraph-checkpoint': ^1.0.0
|
||||
|
||||
'@langchain/langgraph-checkpoint@1.1.3':
|
||||
resolution: {integrity: sha512-wgzdQNeEsdw1e+4lvlj0tdq/RYR/k1vPin10g0ymGoehZDDgd9nvIllGXSXN4TFgF9sf5qQP/KTkOcLfeseIhA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@langchain/core': ^1.1.48
|
||||
|
||||
'@langchain/langgraph-sdk@1.9.28':
|
||||
resolution: {integrity: sha512-4j3XuM0PvtmAbL8mPfBS99ez3+ytRfgbOpAR/nOeaejTRF3Q9dNw2QnaGLGng8wLPtGLoSj+SYgUOVxy9Bv9vg==}
|
||||
peerDependencies:
|
||||
'@langchain/core': ^1.1.48
|
||||
react: ^18 || ^19
|
||||
react-dom: ^18 || ^19
|
||||
svelte: ^4.0.0 || ^5.0.0
|
||||
vue: ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
svelte:
|
||||
optional: true
|
||||
vue:
|
||||
optional: true
|
||||
|
||||
'@langchain/langgraph@1.4.8':
|
||||
resolution: {integrity: sha512-DN1Np1XefdBEbp1qBKlt39cwoL743AAGpR5Ipja0gY2YbWvsoQnOTIrjnj/orSAhaUYsdTKS8VSWdFzsHZo6Ig==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@langchain/core': ^1.1.48
|
||||
zod: ^3.25.32 || ^4.2.0
|
||||
|
||||
'@langchain/protocol@0.0.18':
|
||||
resolution: {integrity: sha512-XW1egQtPfsGI41w2AMZNFZrUIwFSQHTjVMZs0OaTpCAvht/QLoaPN8FQcsysMVypOhupG28J29yOorrc70otBQ==}
|
||||
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||
|
||||
@@ -866,6 +923,9 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@standard-schema/spec@1.1.0':
|
||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||
|
||||
'@tybys/wasm-util@0.10.2':
|
||||
resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
|
||||
|
||||
@@ -887,6 +947,9 @@ packages:
|
||||
'@types/http-proxy@1.17.17':
|
||||
resolution: {integrity: sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==}
|
||||
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
'@types/node@14.18.63':
|
||||
resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==}
|
||||
|
||||
@@ -1189,6 +1252,9 @@ packages:
|
||||
eventemitter3@4.0.7:
|
||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||
|
||||
eventemitter3@5.0.4:
|
||||
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
|
||||
|
||||
exceljs@4.4.0:
|
||||
resolution: {integrity: sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==}
|
||||
engines: {node: '>=8.3.0'}
|
||||
@@ -1372,6 +1438,10 @@ packages:
|
||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-network-error@1.3.2:
|
||||
resolution: {integrity: sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
is-number@7.0.0:
|
||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
@@ -1386,6 +1456,9 @@ packages:
|
||||
isarray@1.0.0:
|
||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||
|
||||
js-tiktoken@1.0.21:
|
||||
resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==}
|
||||
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
@@ -1406,6 +1479,26 @@ packages:
|
||||
jszip@3.10.1:
|
||||
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
||||
|
||||
langsmith@0.8.6:
|
||||
resolution: {integrity: sha512-tl7cV79S/SkJNskCo7joRIVCYy+ljVLs4L1hgC2qT01x4CAWX+6IJzmfXGYAJcn1ecpaQr63WRjkLO9jefmOmQ==}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': '*'
|
||||
'@opentelemetry/exporter-trace-otlp-proto': '*'
|
||||
'@opentelemetry/sdk-trace-base': '*'
|
||||
openai: '*'
|
||||
ws: '>=7'
|
||||
peerDependenciesMeta:
|
||||
'@opentelemetry/api':
|
||||
optional: true
|
||||
'@opentelemetry/exporter-trace-otlp-proto':
|
||||
optional: true
|
||||
'@opentelemetry/sdk-trace-base':
|
||||
optional: true
|
||||
openai:
|
||||
optional: true
|
||||
ws:
|
||||
optional: true
|
||||
|
||||
lazystream@1.0.1:
|
||||
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
|
||||
engines: {node: '>= 0.6.3'}
|
||||
@@ -1533,6 +1626,10 @@ packages:
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
mustache@4.2.0:
|
||||
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
|
||||
hasBin: true
|
||||
|
||||
mysql2@3.22.5:
|
||||
resolution: {integrity: sha512-95uZ2TrPWAZdwpB3vvvDbmEMcNG8yIeNCyu6GUcr/QnWEE/wXm7+mhOCsdQfWQDTV7qYT/PDUZ4U4UPP4AsXqQ==}
|
||||
engines: {node: '>= 8.0'}
|
||||
@@ -1571,6 +1668,10 @@ packages:
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
p-finally@1.0.0:
|
||||
resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
p-limit@2.3.0:
|
||||
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1579,6 +1680,26 @@ packages:
|
||||
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
p-queue@6.6.2:
|
||||
resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
p-queue@9.3.3:
|
||||
resolution: {integrity: sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
p-retry@7.1.1:
|
||||
resolution: {integrity: sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
p-timeout@3.2.0:
|
||||
resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
p-timeout@7.0.1:
|
||||
resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
p-try@2.2.0:
|
||||
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2027,6 +2148,9 @@ packages:
|
||||
resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
zod@4.4.3:
|
||||
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@babel/code-frame@7.29.7':
|
||||
@@ -2141,6 +2265,8 @@ snapshots:
|
||||
'@babel/helper-string-parser': 7.29.7
|
||||
'@babel/helper-validator-identifier': 7.29.7
|
||||
|
||||
'@cfworker/json-schema@4.1.1': {}
|
||||
|
||||
'@emnapi/core@1.11.1':
|
||||
dependencies:
|
||||
'@emnapi/wasi-threads': 1.2.2
|
||||
@@ -2379,6 +2505,61 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@langchain/core@1.2.3(ws@8.21.0)':
|
||||
dependencies:
|
||||
'@cfworker/json-schema': 4.1.1
|
||||
'@standard-schema/spec': 1.1.0
|
||||
js-tiktoken: 1.0.21
|
||||
langsmith: 0.8.6(ws@8.21.0)
|
||||
mustache: 4.2.0
|
||||
p-queue: 6.6.2
|
||||
zod: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- '@opentelemetry/api'
|
||||
- '@opentelemetry/exporter-trace-otlp-proto'
|
||||
- '@opentelemetry/sdk-trace-base'
|
||||
- openai
|
||||
- ws
|
||||
|
||||
'@langchain/langgraph-checkpoint-postgres@1.0.4(@langchain/core@1.2.3(ws@8.21.0))(@langchain/langgraph-checkpoint@1.1.3(@langchain/core@1.2.3(ws@8.21.0)))':
|
||||
dependencies:
|
||||
'@langchain/core': 1.2.3(ws@8.21.0)
|
||||
'@langchain/langgraph-checkpoint': 1.1.3(@langchain/core@1.2.3(ws@8.21.0))
|
||||
pg: 8.22.0
|
||||
transitivePeerDependencies:
|
||||
- pg-native
|
||||
|
||||
'@langchain/langgraph-checkpoint@1.1.3(@langchain/core@1.2.3(ws@8.21.0))':
|
||||
dependencies:
|
||||
'@langchain/core': 1.2.3(ws@8.21.0)
|
||||
|
||||
'@langchain/langgraph-sdk@1.9.28(@langchain/core@1.2.3(ws@8.21.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||
dependencies:
|
||||
'@langchain/core': 1.2.3(ws@8.21.0)
|
||||
'@langchain/protocol': 0.0.18
|
||||
'@types/json-schema': 7.0.15
|
||||
p-queue: 9.3.3
|
||||
p-retry: 7.1.1
|
||||
optionalDependencies:
|
||||
react: 19.2.7
|
||||
react-dom: 19.2.7(react@19.2.7)
|
||||
|
||||
'@langchain/langgraph@1.4.8(@langchain/core@1.2.3(ws@8.21.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3)':
|
||||
dependencies:
|
||||
'@langchain/core': 1.2.3(ws@8.21.0)
|
||||
'@langchain/langgraph-checkpoint': 1.1.3(@langchain/core@1.2.3(ws@8.21.0))
|
||||
'@langchain/langgraph-sdk': 1.9.28(@langchain/core@1.2.3(ws@8.21.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
'@langchain/protocol': 0.0.18
|
||||
'@standard-schema/spec': 1.1.0
|
||||
zod: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- react
|
||||
- react-dom
|
||||
- svelte
|
||||
- vue
|
||||
|
||||
'@langchain/protocol@0.0.18': {}
|
||||
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.11.1
|
||||
@@ -2601,6 +2782,8 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc@4.62.0':
|
||||
optional: true
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@tybys/wasm-util@0.10.2':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -2633,6 +2816,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 25.9.3
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/node@14.18.63': {}
|
||||
|
||||
'@types/node@25.9.3':
|
||||
@@ -2951,6 +3136,8 @@ snapshots:
|
||||
|
||||
eventemitter3@4.0.7: {}
|
||||
|
||||
eventemitter3@5.0.4: {}
|
||||
|
||||
exceljs@4.4.0:
|
||||
dependencies:
|
||||
archiver: 5.3.2
|
||||
@@ -3181,6 +3368,8 @@ snapshots:
|
||||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
|
||||
is-network-error@1.3.2: {}
|
||||
|
||||
is-number@7.0.0: {}
|
||||
|
||||
is-plain-object@5.0.0: {}
|
||||
@@ -3189,6 +3378,10 @@ snapshots:
|
||||
|
||||
isarray@1.0.0: {}
|
||||
|
||||
js-tiktoken@1.0.21:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
@@ -3204,6 +3397,12 @@ snapshots:
|
||||
readable-stream: 2.3.8
|
||||
setimmediate: 1.0.5
|
||||
|
||||
langsmith@0.8.6(ws@8.21.0):
|
||||
dependencies:
|
||||
p-queue: 6.6.2
|
||||
optionalDependencies:
|
||||
ws: 8.21.0
|
||||
|
||||
lazystream@1.0.1:
|
||||
dependencies:
|
||||
readable-stream: 2.3.8
|
||||
@@ -3301,6 +3500,8 @@ snapshots:
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
mustache@4.2.0: {}
|
||||
|
||||
mysql2@3.22.5(@types/node@25.9.3):
|
||||
dependencies:
|
||||
'@types/node': 25.9.3
|
||||
@@ -3335,6 +3536,8 @@ snapshots:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
p-finally@1.0.0: {}
|
||||
|
||||
p-limit@2.3.0:
|
||||
dependencies:
|
||||
p-try: 2.2.0
|
||||
@@ -3343,6 +3546,26 @@ snapshots:
|
||||
dependencies:
|
||||
p-limit: 2.3.0
|
||||
|
||||
p-queue@6.6.2:
|
||||
dependencies:
|
||||
eventemitter3: 4.0.7
|
||||
p-timeout: 3.2.0
|
||||
|
||||
p-queue@9.3.3:
|
||||
dependencies:
|
||||
eventemitter3: 5.0.4
|
||||
p-timeout: 7.0.1
|
||||
|
||||
p-retry@7.1.1:
|
||||
dependencies:
|
||||
is-network-error: 1.3.2
|
||||
|
||||
p-timeout@3.2.0:
|
||||
dependencies:
|
||||
p-finally: 1.0.0
|
||||
|
||||
p-timeout@7.0.1: {}
|
||||
|
||||
p-try@2.2.0: {}
|
||||
|
||||
pako@1.0.11: {}
|
||||
@@ -3803,3 +4026,5 @@ snapshots:
|
||||
archiver-utils: 3.0.4
|
||||
compress-commons: 4.1.2
|
||||
readable-stream: 3.6.2
|
||||
|
||||
zod@4.4.3: {}
|
||||
|
||||
@@ -213,6 +213,8 @@ import { createSessionSnapshotService } from './session-snapshot.mjs';
|
||||
import { createConversationMemoryService } from './conversation-memory.mjs';
|
||||
import { createManagedMemoryV2Runtime } from './memory-v2-runtime.mjs';
|
||||
import { createMemoryV2AdminConfigService } from './memory-v2-admin-config.mjs';
|
||||
import { createOrchestratorAdminConfigService } from './services/orchestrator/admin-config.mjs';
|
||||
import { createWorkflowShadowObserver } from './services/orchestrator/shadow-observer.mjs';
|
||||
import { createEpisodicMemoryService } from './episodic-memory.mjs';
|
||||
import { createSkillRuntimeAdminConfigService } from './skill-runtime-admin-config.mjs';
|
||||
import { createSystemDisclosurePolicyService } from './system-disclosure-policy.mjs';
|
||||
@@ -798,6 +800,10 @@ async function bootstrapUserAuth() {
|
||||
: null,
|
||||
});
|
||||
toolGateway = createToolGateway({ llmProviderService });
|
||||
const workflowShadowObserver = createWorkflowShadowObserver({
|
||||
configService: createOrchestratorAdminConfigService(pool),
|
||||
logger: console,
|
||||
});
|
||||
agentRunGateway = createAgentRunGateway({
|
||||
pool,
|
||||
userAuth,
|
||||
@@ -809,6 +815,7 @@ async function bootstrapUserAuth() {
|
||||
chatIntentRouter,
|
||||
sessionSnapshotService,
|
||||
conversationMemoryService,
|
||||
observeWorkflowRun: workflowShadowObserver,
|
||||
observePersonalMemoryOnSuccess: async ({ userId, sessionId, userMessage }) => {
|
||||
if (!memoryV2?.observePersonalMemory) return;
|
||||
await memoryV2.observePersonalMemory({
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
FROM node:24-bookworm-slim
|
||||
|
||||
ENV NODE_ENV=production
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
COPY *.mjs ./
|
||||
|
||||
USER node
|
||||
EXPOSE 8093
|
||||
HEALTHCHECK --interval=15s --timeout=3s --start-period=10s --retries=3 \
|
||||
CMD node -e "fetch('http://127.0.0.1:8093/ready').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
|
||||
|
||||
CMD ["node", "server.mjs"]
|
||||
@@ -29,7 +29,7 @@ Memind-facing protocol is `orchestrator-run-v1` plus `orchestrator-event-v1`.
|
||||
|
||||
## Current stage
|
||||
|
||||
Phase 1 establishes:
|
||||
Phase 1 established:
|
||||
|
||||
- framework-neutral run and event contracts;
|
||||
- a plug-in workflow engine registry;
|
||||
@@ -38,9 +38,19 @@ Phase 1 establishes:
|
||||
- deterministic Off / Shadow / Canary / Active routing decisions;
|
||||
- an environment-level emergency kill switch.
|
||||
|
||||
The default mode is `off`. No production task is routed to LangGraph in this
|
||||
phase. Runtime dispatch wiring and a separately deployed Orchestrator worker are
|
||||
subsequent phases.
|
||||
Phase 2 adds:
|
||||
|
||||
- a real LangGraph `StateGraph` service on the versioned HTTP boundary;
|
||||
- a PostgreSQL `PostgresSaver` checkpoint owner;
|
||||
- an explicitly non-durable MemorySaver option for tests and local debugging;
|
||||
- a `code-run-v1` observe-only graph with no executor or tool access;
|
||||
- a Portal fire-and-forget shadow projection;
|
||||
- live health probing from memindadm;
|
||||
- a separate Colima/Compose deployment artifact.
|
||||
|
||||
The default memindadm mode remains `off`. Canary and Active routing are not
|
||||
wired to the LangGraph executor in Phase 2. Native Agent Run remains the sole
|
||||
executor, including in Shadow mode.
|
||||
|
||||
## Admin configuration
|
||||
|
||||
@@ -55,6 +65,62 @@ Supported modes:
|
||||
|
||||
`MEMIND_ORCHESTRATOR_KILL_SWITCH=1` always forces Native selection.
|
||||
|
||||
## Local process
|
||||
|
||||
PostgreSQL is the default and required checkpoint backend:
|
||||
|
||||
```bash
|
||||
MEMIND_ORCHESTRATOR_DATABASE_URL='postgresql://...' \
|
||||
MEMIND_ORCHESTRATOR_SERVICE_TOKEN='...' \
|
||||
pnpm dev:orchestrator
|
||||
```
|
||||
|
||||
Memory mode is intentionally opt-in and non-durable:
|
||||
|
||||
```bash
|
||||
MEMIND_ORCHESTRATOR_CHECKPOINT_MODE=memory pnpm dev:orchestrator
|
||||
```
|
||||
|
||||
The service binds to `127.0.0.1:8093` by default. Configure the same URL and
|
||||
service token in Portal, then enable `shadow` in `/ops/admin/orchestrator`.
|
||||
|
||||
## Colima deployment
|
||||
|
||||
Colima is the recommended first container host on macOS because this service and
|
||||
its PostgreSQL checkpoint database have an isolated Compose lifecycle:
|
||||
|
||||
```bash
|
||||
cd deploy/orchestrator
|
||||
cp .env.example .env
|
||||
# Replace both example secrets before starting.
|
||||
docker compose up -d --build
|
||||
docker compose ps
|
||||
curl http://127.0.0.1:8093/ready
|
||||
```
|
||||
|
||||
The Compose project:
|
||||
|
||||
- publishes only the Orchestrator port on loopback;
|
||||
- does not publish PostgreSQL;
|
||||
- does not mount the Docker socket or a Memind workspace;
|
||||
- keeps PostgreSQL on a dedicated internal network while the service also joins
|
||||
a separate edge network for its loopback-published port;
|
||||
- is not part of the Goosed Compose lifecycle.
|
||||
|
||||
This local Compose artifact is not a production release path. Production still
|
||||
requires the repository release gates and a separately approved deployment.
|
||||
|
||||
## Runtime environment
|
||||
|
||||
| Variable | Purpose | Default |
|
||||
|---|---|---|
|
||||
| `MEMIND_ORCHESTRATOR_HOST` | HTTP bind address | `127.0.0.1` |
|
||||
| `MEMIND_ORCHESTRATOR_PORT` | HTTP port | `8093` |
|
||||
| `MEMIND_ORCHESTRATOR_SERVICE_TOKEN` | Bearer token for `/v1/*` | empty |
|
||||
| `MEMIND_ORCHESTRATOR_CHECKPOINT_MODE` | `postgres` or explicit `memory` | `postgres` |
|
||||
| `MEMIND_ORCHESTRATOR_DATABASE_URL` | Dedicated checkpoint PostgreSQL URL | required |
|
||||
| `MEMIND_ORCHESTRATOR_DATABASE_SCHEMA` | Checkpoint schema | `memind_orchestrator` |
|
||||
|
||||
## Extraction test
|
||||
|
||||
The service is ready to move into a separate repository only when:
|
||||
|
||||
@@ -146,6 +146,72 @@ function engineCatalog(config) {
|
||||
];
|
||||
}
|
||||
|
||||
async function probeServiceHealth(config, {
|
||||
fetchImpl = globalThis.fetch,
|
||||
} = {}) {
|
||||
const checkedAt = Date.now();
|
||||
if (!config.serviceUrl) {
|
||||
return {
|
||||
checkedAt,
|
||||
ok: false,
|
||||
status: 'unconfigured',
|
||||
latencyMs: 0,
|
||||
httpStatus: null,
|
||||
details: null,
|
||||
};
|
||||
}
|
||||
if (typeof fetchImpl !== 'function') {
|
||||
return {
|
||||
checkedAt,
|
||||
ok: false,
|
||||
status: 'fetch_unavailable',
|
||||
latencyMs: 0,
|
||||
httpStatus: null,
|
||||
details: null,
|
||||
};
|
||||
}
|
||||
const startedAt = Date.now();
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(
|
||||
() => controller.abort(),
|
||||
Math.max(500, Number(config.requestTimeoutMs) || 5000),
|
||||
);
|
||||
try {
|
||||
const response = await fetchImpl(`${config.serviceUrl}/ready`, {
|
||||
headers: { accept: 'application/json' },
|
||||
signal: controller.signal,
|
||||
});
|
||||
const body = await response.json().catch(() => null);
|
||||
const healthy = response.ok && body?.status === 'ok';
|
||||
return {
|
||||
checkedAt,
|
||||
ok: healthy,
|
||||
status: healthy ? 'healthy' : 'unhealthy',
|
||||
latencyMs: Math.max(0, Date.now() - startedAt),
|
||||
httpStatus: response.status,
|
||||
details: body && typeof body === 'object' ? {
|
||||
service: body.service == null ? null : String(body.service).slice(0, 128),
|
||||
checkpoint: body.checkpoint && typeof body.checkpoint === 'object' ? {
|
||||
kind: body.checkpoint.kind == null ? null : String(body.checkpoint.kind).slice(0, 64),
|
||||
durable: Boolean(body.checkpoint.durable),
|
||||
} : null,
|
||||
execution: body.execution == null ? null : String(body.execution).slice(0, 64),
|
||||
} : null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
checkedAt,
|
||||
ok: false,
|
||||
status: error?.name === 'AbortError' ? 'timeout' : 'unreachable',
|
||||
latencyMs: Math.max(0, Date.now() - startedAt),
|
||||
httpStatus: null,
|
||||
details: null,
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureConfigTable(pool) {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS ${CONFIG_TABLE} (
|
||||
@@ -177,7 +243,10 @@ async function loadStoredState(pool) {
|
||||
};
|
||||
}
|
||||
|
||||
export function createOrchestratorAdminConfigService(pool, { env = process.env } = {}) {
|
||||
export function createOrchestratorAdminConfigService(pool, {
|
||||
env = process.env,
|
||||
fetchImpl = globalThis.fetch,
|
||||
} = {}) {
|
||||
async function loadEffectiveState() {
|
||||
const stored = await loadStoredState(pool);
|
||||
if (stored) return { ...stored, source: 'admin-db' };
|
||||
@@ -232,12 +301,13 @@ export function createOrchestratorAdminConfigService(pool, { env = process.env }
|
||||
return this.getAdminConfig();
|
||||
},
|
||||
|
||||
async getRuntimeState() {
|
||||
async getRuntimeState({ probe = false } = {}) {
|
||||
const state = await loadEffectiveState();
|
||||
return {
|
||||
...state,
|
||||
runtime: runtimeState(state.config, env),
|
||||
engines: engineCatalog(state.config),
|
||||
...(probe ? { serviceHealth: await probeServiceHealth(state.config, { fetchImpl }) } : {}),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -292,5 +362,6 @@ export const orchestratorAdminConfigInternals = {
|
||||
CONFIG_SCOPE,
|
||||
CONFIG_TABLE,
|
||||
engineCatalog,
|
||||
probeServiceHealth,
|
||||
runtimeState,
|
||||
};
|
||||
|
||||
@@ -97,3 +97,34 @@ test('orchestrator emergency kill switch always forces native selection', async
|
||||
assert.equal(selected.engine, 'native');
|
||||
assert.equal(selected.reason, 'kill_switch');
|
||||
});
|
||||
|
||||
test('orchestrator runtime probe reports sanitized service health', async () => {
|
||||
let requestedUrl = null;
|
||||
const service = createOrchestratorAdminConfigService(createPool(), {
|
||||
env: {},
|
||||
fetchImpl: async (url) => {
|
||||
requestedUrl = url;
|
||||
return new Response(JSON.stringify({
|
||||
status: 'ok',
|
||||
service: 'memind-langgraph-orchestrator',
|
||||
checkpoint: { kind: 'postgres', durable: true, password: 'must-not-pass-through' },
|
||||
execution: 'observe-only',
|
||||
ignoredSecret: 'must-not-pass-through',
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
},
|
||||
});
|
||||
await service.updateAdminConfig({
|
||||
mode: 'shadow',
|
||||
serviceUrl: 'http://127.0.0.1:8093',
|
||||
});
|
||||
|
||||
const runtime = await service.getRuntimeState({ probe: true });
|
||||
assert.equal(requestedUrl, 'http://127.0.0.1:8093/ready');
|
||||
assert.equal(runtime.serviceHealth.ok, true);
|
||||
assert.equal(runtime.serviceHealth.details.checkpoint.durable, true);
|
||||
assert.equal('password' in runtime.serviceHealth.details.checkpoint, false);
|
||||
assert.equal('ignoredSecret' in runtime.serviceHealth.details, false);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import express from 'express';
|
||||
|
||||
function errorStatus(error) {
|
||||
if (Number.isInteger(error?.status)) return error.status;
|
||||
if (['WORKFLOW_NOT_SUPPORTED', 'WORKFLOW_OBSERVE_ONLY_REQUIRED'].includes(error?.code)) {
|
||||
return 422;
|
||||
}
|
||||
return 500;
|
||||
}
|
||||
|
||||
function authorize(serviceToken) {
|
||||
const expected = String(serviceToken ?? '').trim();
|
||||
return (request, response, next) => {
|
||||
if (!expected) return next();
|
||||
if (request.get('authorization') === `Bearer ${expected}`) return next();
|
||||
return response.status(401).json({
|
||||
error: {
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'Missing or invalid service token',
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function createOrchestratorApp({
|
||||
runtime,
|
||||
serviceToken = process.env.MEMIND_ORCHESTRATOR_SERVICE_TOKEN,
|
||||
} = {}) {
|
||||
if (!runtime) throw new Error('Orchestrator app requires runtime');
|
||||
const app = express();
|
||||
app.disable('x-powered-by');
|
||||
app.use(express.json({ limit: '256kb' }));
|
||||
|
||||
app.get('/health', (_request, response) => response.json(runtime.health()));
|
||||
app.get('/ready', async (_request, response) => {
|
||||
try {
|
||||
return response.json(await runtime.ready());
|
||||
} catch {
|
||||
return response.status(503).json({
|
||||
...runtime.health(),
|
||||
ready: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.use('/v1', authorize(serviceToken));
|
||||
|
||||
app.post('/v1/runs', async (request, response, next) => {
|
||||
try {
|
||||
response.status(202).json(await runtime.start(request.body));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/v1/runs/:runId', async (request, response, next) => {
|
||||
try {
|
||||
const state = await runtime.getState(request.params.runId);
|
||||
if (!state) return response.status(404).json({ error: { code: 'RUN_NOT_FOUND' } });
|
||||
return response.json(state);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/v1/runs/:runId/events', async (request, response, next) => {
|
||||
try {
|
||||
const result = await runtime.listEvents(request.params.runId, {
|
||||
after: request.query.after,
|
||||
});
|
||||
if (!result) return response.status(404).json({ error: { code: 'RUN_NOT_FOUND' } });
|
||||
return response.json(result);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/v1/runs/:runId/resume', async (request, response, next) => {
|
||||
try {
|
||||
const state = await runtime.resume(request.params.runId, request.body);
|
||||
if (!state) return response.status(404).json({ error: { code: 'RUN_NOT_FOUND' } });
|
||||
return response.json(state);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/v1/runs/:runId/cancel', async (request, response, next) => {
|
||||
try {
|
||||
const state = await runtime.cancel(request.params.runId, request.body);
|
||||
if (!state) return response.status(404).json({ error: { code: 'RUN_NOT_FOUND' } });
|
||||
return response.json(state);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.use((error, _request, response, _next) => {
|
||||
response.status(errorStatus(error)).json({
|
||||
error: {
|
||||
code: error?.code ?? 'ORCHESTRATOR_ERROR',
|
||||
message: error instanceof Error ? error.message : 'Orchestrator request failed',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
export const orchestratorAppInternals = {
|
||||
authorize,
|
||||
errorStatus,
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { MemorySaver } from '@langchain/langgraph';
|
||||
import { createOrchestratorApp } from './app.mjs';
|
||||
import { createLangGraphOrchestratorRuntime } from './runtime.mjs';
|
||||
|
||||
async function listen(app) {
|
||||
const server = await new Promise((resolve) => {
|
||||
const candidate = app.listen(0, '127.0.0.1', () => resolve(candidate));
|
||||
});
|
||||
const address = server.address();
|
||||
return {
|
||||
baseUrl: `http://127.0.0.1:${address.port}`,
|
||||
close: () => new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function spec() {
|
||||
return {
|
||||
runId: 'http-shadow-1',
|
||||
requestId: 'http-request-1',
|
||||
workflow: { name: 'code-run-v1', version: 1 },
|
||||
input: { instruction: 'Inspect without executing' },
|
||||
policy: { executionMode: 'observe-only', sideEffectsAllowed: false },
|
||||
};
|
||||
}
|
||||
|
||||
test('orchestrator HTTP API exposes health and authenticated run endpoints', async (t) => {
|
||||
const runtime = createLangGraphOrchestratorRuntime({
|
||||
checkpointer: new MemorySaver(),
|
||||
checkpointKind: 'memory',
|
||||
});
|
||||
const server = await listen(createOrchestratorApp({
|
||||
runtime,
|
||||
serviceToken: 'test-service-token',
|
||||
}));
|
||||
t.after(server.close);
|
||||
|
||||
const health = await fetch(`${server.baseUrl}/health`);
|
||||
assert.equal(health.status, 200);
|
||||
assert.equal((await health.json()).execution, 'observe-only');
|
||||
const ready = await fetch(`${server.baseUrl}/ready`);
|
||||
assert.equal(ready.status, 200);
|
||||
assert.equal((await ready.json()).ready, true);
|
||||
|
||||
const unauthorized = await fetch(`${server.baseUrl}/v1/runs`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(spec()),
|
||||
});
|
||||
assert.equal(unauthorized.status, 401);
|
||||
|
||||
const created = await fetch(`${server.baseUrl}/v1/runs`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
authorization: 'Bearer test-service-token',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(spec()),
|
||||
});
|
||||
assert.equal(created.status, 202);
|
||||
assert.equal((await created.json()).status, 'succeeded');
|
||||
|
||||
const events = await fetch(`${server.baseUrl}/v1/runs/http-shadow-1/events?after=1`, {
|
||||
headers: { authorization: 'Bearer test-service-token' },
|
||||
});
|
||||
const eventBody = await events.json();
|
||||
assert.equal(events.status, 200);
|
||||
assert.deepEqual(eventBody.events.map((event) => event.sequence), [2, 3]);
|
||||
});
|
||||
|
||||
test('orchestrator HTTP API reports missing runs and observe-only violations', async (t) => {
|
||||
const runtime = createLangGraphOrchestratorRuntime({
|
||||
checkpointer: new MemorySaver(),
|
||||
});
|
||||
const server = await listen(createOrchestratorApp({ runtime }));
|
||||
t.after(server.close);
|
||||
|
||||
const missing = await fetch(`${server.baseUrl}/v1/runs/missing`);
|
||||
assert.equal(missing.status, 404);
|
||||
|
||||
const activeSpec = spec();
|
||||
activeSpec.runId = 'http-active-1';
|
||||
activeSpec.policy = { executionMode: 'active', sideEffectsAllowed: true };
|
||||
const rejected = await fetch(`${server.baseUrl}/v1/runs`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(activeSpec),
|
||||
});
|
||||
assert.equal(rejected.status, 422);
|
||||
assert.equal((await rejected.json()).error.code, 'WORKFLOW_OBSERVE_ONLY_REQUIRED');
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import { MemorySaver } from '@langchain/langgraph';
|
||||
import { PostgresSaver } from '@langchain/langgraph-checkpoint-postgres';
|
||||
|
||||
const DEFAULT_POSTGRES_SCHEMA = 'memind_orchestrator';
|
||||
|
||||
function normalizeMode(value) {
|
||||
const mode = String(value ?? 'postgres').trim().toLowerCase();
|
||||
if (mode === 'memory' || mode === 'postgres') return mode;
|
||||
throw new Error(`Unsupported orchestrator checkpoint mode: ${value}`);
|
||||
}
|
||||
|
||||
function normalizeSchema(value) {
|
||||
const schema = String(value ?? DEFAULT_POSTGRES_SCHEMA).trim();
|
||||
if (!/^[a-z_][a-z0-9_]{0,62}$/i.test(schema)) {
|
||||
throw new Error('Invalid orchestrator PostgreSQL schema');
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
export async function createOrchestratorCheckpoint({
|
||||
mode = process.env.MEMIND_ORCHESTRATOR_CHECKPOINT_MODE,
|
||||
connectionString = process.env.MEMIND_ORCHESTRATOR_DATABASE_URL,
|
||||
schema = process.env.MEMIND_ORCHESTRATOR_DATABASE_SCHEMA,
|
||||
} = {}) {
|
||||
const normalizedMode = normalizeMode(mode);
|
||||
if (normalizedMode === 'memory') {
|
||||
return {
|
||||
kind: 'memory',
|
||||
durable: false,
|
||||
checkpointer: new MemorySaver(),
|
||||
async probe() {
|
||||
return true;
|
||||
},
|
||||
async close() {},
|
||||
};
|
||||
}
|
||||
|
||||
const normalizedConnectionString = String(connectionString ?? '').trim();
|
||||
if (!normalizedConnectionString) {
|
||||
const error = new Error(
|
||||
'MEMIND_ORCHESTRATOR_DATABASE_URL is required when checkpoint mode is postgres',
|
||||
);
|
||||
error.code = 'ORCHESTRATOR_DATABASE_URL_REQUIRED';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const checkpointer = PostgresSaver.fromConnString(normalizedConnectionString, {
|
||||
schema: normalizeSchema(schema),
|
||||
});
|
||||
await checkpointer.setup();
|
||||
return {
|
||||
kind: 'postgres',
|
||||
durable: true,
|
||||
checkpointer,
|
||||
async probe() {
|
||||
await checkpointer.getTuple({
|
||||
configurable: {
|
||||
thread_id: '__orchestrator_readiness__',
|
||||
checkpoint_ns: '',
|
||||
},
|
||||
});
|
||||
return true;
|
||||
},
|
||||
async close() {
|
||||
await checkpointer.end();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const orchestratorCheckpointInternals = {
|
||||
DEFAULT_POSTGRES_SCHEMA,
|
||||
normalizeMode,
|
||||
normalizeSchema,
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
createOrchestratorCheckpoint,
|
||||
orchestratorCheckpointInternals,
|
||||
} from './checkpoint.mjs';
|
||||
|
||||
test('checkpoint defaults to durable PostgreSQL and fails closed without a URL', async () => {
|
||||
await assert.rejects(
|
||||
createOrchestratorCheckpoint({ mode: undefined, connectionString: '' }),
|
||||
(error) => error.code === 'ORCHESTRATOR_DATABASE_URL_REQUIRED',
|
||||
);
|
||||
});
|
||||
|
||||
test('memory checkpoint is explicitly non-durable', async () => {
|
||||
const checkpoint = await createOrchestratorCheckpoint({ mode: 'memory' });
|
||||
assert.equal(checkpoint.kind, 'memory');
|
||||
assert.equal(checkpoint.durable, false);
|
||||
assert.equal(typeof checkpoint.checkpointer.getTuple, 'function');
|
||||
await checkpoint.close();
|
||||
});
|
||||
|
||||
test('checkpoint schema accepts identifiers and rejects SQL fragments', () => {
|
||||
assert.equal(
|
||||
orchestratorCheckpointInternals.normalizeSchema('memind_orchestrator_v2'),
|
||||
'memind_orchestrator_v2',
|
||||
);
|
||||
assert.throws(
|
||||
() => orchestratorCheckpointInternals.normalizeSchema('public; drop schema public'),
|
||||
/Invalid orchestrator PostgreSQL schema/,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
Annotation,
|
||||
END,
|
||||
START,
|
||||
StateGraph,
|
||||
} from '@langchain/langgraph';
|
||||
import {
|
||||
buildRunEvent,
|
||||
normalizeRunSpec,
|
||||
} from './contracts.mjs';
|
||||
|
||||
export const CODE_RUN_SHADOW_WORKFLOW = 'code-run-v1';
|
||||
|
||||
const ShadowState = Annotation.Root({
|
||||
spec: Annotation(),
|
||||
status: Annotation({
|
||||
reducer: (_current, update) => update,
|
||||
default: () => 'queued',
|
||||
}),
|
||||
phase: Annotation({
|
||||
reducer: (_current, update) => update,
|
||||
default: () => 'accepted',
|
||||
}),
|
||||
plan: Annotation({
|
||||
reducer: (_current, update) => update,
|
||||
default: () => null,
|
||||
}),
|
||||
result: Annotation({
|
||||
reducer: (_current, update) => update,
|
||||
default: () => null,
|
||||
}),
|
||||
events: Annotation({
|
||||
reducer: (current, update) => [...current, ...update],
|
||||
default: () => [],
|
||||
}),
|
||||
});
|
||||
|
||||
function eventFor(state, type, data = null) {
|
||||
return buildRunEvent({
|
||||
runId: state.spec.runId,
|
||||
sequence: state.events.length + 1,
|
||||
type,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
function validateNode(state) {
|
||||
const spec = normalizeRunSpec(state.spec);
|
||||
if (spec.workflow.name !== CODE_RUN_SHADOW_WORKFLOW) {
|
||||
const error = new Error(`Unsupported workflow: ${spec.workflow.name}`);
|
||||
error.code = 'WORKFLOW_NOT_SUPPORTED';
|
||||
throw error;
|
||||
}
|
||||
if (spec.policy.executionMode !== 'observe-only' || spec.policy.sideEffectsAllowed !== false) {
|
||||
const error = new Error('Phase 2 LangGraph service accepts observe-only runs');
|
||||
error.code = 'WORKFLOW_OBSERVE_ONLY_REQUIRED';
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
spec,
|
||||
status: 'running',
|
||||
phase: 'validated',
|
||||
events: [eventFor(state, 'workflow_validated', {
|
||||
workflow: spec.workflow,
|
||||
executionMode: 'observe-only',
|
||||
})],
|
||||
};
|
||||
}
|
||||
|
||||
function planNode(state) {
|
||||
const taskType = String(state.spec.input?.taskType ?? '').trim() || 'code-change';
|
||||
const instruction = String(state.spec.input?.instruction ?? '');
|
||||
const plan = {
|
||||
taskType,
|
||||
executorAdapter: 'native-agent-run',
|
||||
instructionCharacters: instruction.length,
|
||||
steps: [
|
||||
'accept_control_plane_run',
|
||||
'project_executor_boundary',
|
||||
'record_shadow_result',
|
||||
],
|
||||
};
|
||||
return {
|
||||
phase: 'planned',
|
||||
plan,
|
||||
events: [eventFor(state, 'workflow_planned', {
|
||||
taskType,
|
||||
executorAdapter: plan.executorAdapter,
|
||||
stepCount: plan.steps.length,
|
||||
})],
|
||||
};
|
||||
}
|
||||
|
||||
function finalizeNode(state) {
|
||||
const result = {
|
||||
observed: true,
|
||||
executed: false,
|
||||
executorAdapter: state.plan.executorAdapter,
|
||||
taskType: state.plan.taskType,
|
||||
};
|
||||
return {
|
||||
status: 'succeeded',
|
||||
phase: 'completed',
|
||||
result,
|
||||
events: [eventFor(state, 'workflow_completed', result)],
|
||||
};
|
||||
}
|
||||
|
||||
export function createCodeRunShadowGraph({ checkpointer } = {}) {
|
||||
if (!checkpointer) throw new Error('Code run shadow graph requires a checkpointer');
|
||||
return new StateGraph(ShadowState)
|
||||
.addNode('validate_run', validateNode)
|
||||
.addNode('build_plan', planNode)
|
||||
.addNode('finalize_run', finalizeNode)
|
||||
.addEdge(START, 'validate_run')
|
||||
.addEdge('validate_run', 'build_plan')
|
||||
.addEdge('build_plan', 'finalize_run')
|
||||
.addEdge('finalize_run', END)
|
||||
.compile({ checkpointer });
|
||||
}
|
||||
|
||||
export const codeRunShadowGraphInternals = {
|
||||
ShadowState,
|
||||
eventFor,
|
||||
validateNode,
|
||||
planNode,
|
||||
finalizeNode,
|
||||
};
|
||||
Generated
+1286
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@memind/workflow-orchestrator",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node server.mjs",
|
||||
"test": "node --test *.test.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langchain/core": "^1.2.3",
|
||||
"@langchain/langgraph": "^1.4.8",
|
||||
"@langchain/langgraph-checkpoint-postgres": "^1.0.4",
|
||||
"express": "^4.21.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { normalizeRunSpec } from './contracts.mjs';
|
||||
import {
|
||||
CODE_RUN_SHADOW_WORKFLOW,
|
||||
createCodeRunShadowGraph,
|
||||
} from './code-run-shadow-graph.mjs';
|
||||
|
||||
function graphConfig(runId) {
|
||||
return {
|
||||
configurable: {
|
||||
thread_id: runId,
|
||||
checkpoint_ns: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function projectSnapshot(runId, snapshot) {
|
||||
const values = snapshot?.values ?? {};
|
||||
if (!values.spec) return null;
|
||||
return {
|
||||
version: 'orchestrator-state-v1',
|
||||
runId,
|
||||
requestId: values.spec.requestId,
|
||||
workflow: values.spec.workflow,
|
||||
status: values.status ?? 'unknown',
|
||||
phase: values.phase ?? null,
|
||||
plan: values.plan ?? null,
|
||||
result: values.result ?? null,
|
||||
createdAt: values.events?.[0]?.timestamp ?? null,
|
||||
updatedAt: values.events?.at?.(-1)?.timestamp ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function createLangGraphOrchestratorRuntime({
|
||||
checkpointer,
|
||||
checkpointKind = 'unknown',
|
||||
durable = false,
|
||||
checkpointProbe = null,
|
||||
} = {}) {
|
||||
const graph = createCodeRunShadowGraph({ checkpointer });
|
||||
const probe = typeof checkpointProbe === 'function'
|
||||
? checkpointProbe
|
||||
: async () => {
|
||||
await checkpointer.getTuple(graphConfig('__orchestrator_readiness__'));
|
||||
return true;
|
||||
};
|
||||
|
||||
async function getSnapshot(runId) {
|
||||
return graph.getState(graphConfig(runId));
|
||||
}
|
||||
|
||||
async function getState(runId) {
|
||||
const normalizedRunId = String(runId ?? '').trim();
|
||||
if (!normalizedRunId) return null;
|
||||
return projectSnapshot(normalizedRunId, await getSnapshot(normalizedRunId));
|
||||
}
|
||||
|
||||
function health() {
|
||||
return {
|
||||
status: 'ok',
|
||||
service: 'memind-langgraph-orchestrator',
|
||||
checkpoint: {
|
||||
kind: checkpointKind,
|
||||
durable: Boolean(durable),
|
||||
},
|
||||
execution: 'observe-only',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
async start(input) {
|
||||
const spec = normalizeRunSpec(input);
|
||||
if (spec.workflow.name !== CODE_RUN_SHADOW_WORKFLOW) {
|
||||
const error = new Error(`Unsupported workflow: ${spec.workflow.name}`);
|
||||
error.code = 'WORKFLOW_NOT_SUPPORTED';
|
||||
error.status = 422;
|
||||
throw error;
|
||||
}
|
||||
const existing = await getState(spec.runId);
|
||||
if (existing) return existing;
|
||||
await graph.invoke({
|
||||
spec,
|
||||
status: 'queued',
|
||||
phase: 'accepted',
|
||||
events: [],
|
||||
}, graphConfig(spec.runId));
|
||||
return getState(spec.runId);
|
||||
},
|
||||
|
||||
getState,
|
||||
|
||||
async listEvents(runId, { after = 0 } = {}) {
|
||||
const normalizedRunId = String(runId ?? '').trim();
|
||||
const snapshot = normalizedRunId ? await getSnapshot(normalizedRunId) : null;
|
||||
if (!snapshot?.values?.spec) return null;
|
||||
const cursor = Math.max(0, Number(after) || 0);
|
||||
const events = (snapshot.values.events ?? []).filter((event) => event.sequence > cursor);
|
||||
return {
|
||||
runId: normalizedRunId,
|
||||
events,
|
||||
nextCursor: events.at(-1)?.sequence ?? cursor,
|
||||
};
|
||||
},
|
||||
|
||||
async resume(runId) {
|
||||
const state = await getState(runId);
|
||||
if (!state) return null;
|
||||
const error = new Error('This workflow has no interrupt point to resume');
|
||||
error.code = 'WORKFLOW_NOT_INTERRUPTED';
|
||||
error.status = 409;
|
||||
throw error;
|
||||
},
|
||||
|
||||
async cancel(runId) {
|
||||
const state = await getState(runId);
|
||||
if (!state) return null;
|
||||
const error = new Error('Completed shadow observations cannot be cancelled');
|
||||
error.code = 'WORKFLOW_ALREADY_TERMINAL';
|
||||
error.status = 409;
|
||||
throw error;
|
||||
},
|
||||
|
||||
health,
|
||||
|
||||
async ready() {
|
||||
await probe();
|
||||
return {
|
||||
...health(),
|
||||
ready: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const orchestratorRuntimeInternals = {
|
||||
graphConfig,
|
||||
projectSnapshot,
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { MemorySaver } from '@langchain/langgraph';
|
||||
import { createLangGraphOrchestratorRuntime } from './runtime.mjs';
|
||||
|
||||
function runSpec(overrides = {}) {
|
||||
return {
|
||||
runId: 'run-shadow-1',
|
||||
requestId: 'request-shadow-1',
|
||||
workflow: { name: 'code-run-v1', version: 1 },
|
||||
subject: { userId: 'user-1' },
|
||||
input: {
|
||||
instruction: 'Add an idempotency test',
|
||||
taskType: 'code-change',
|
||||
},
|
||||
policy: {
|
||||
executionMode: 'observe-only',
|
||||
sideEffectsAllowed: false,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('LangGraph runtime checkpoints a deterministic observe-only code workflow', async () => {
|
||||
const runtime = createLangGraphOrchestratorRuntime({
|
||||
checkpointer: new MemorySaver(),
|
||||
checkpointKind: 'memory',
|
||||
durable: false,
|
||||
});
|
||||
|
||||
const state = await runtime.start(runSpec());
|
||||
assert.equal(state.runId, 'run-shadow-1');
|
||||
assert.equal(state.status, 'succeeded');
|
||||
assert.equal(state.phase, 'completed');
|
||||
assert.equal(state.result.observed, true);
|
||||
assert.equal(state.result.executed, false);
|
||||
assert.equal(state.plan.executorAdapter, 'native-agent-run');
|
||||
|
||||
const restored = await runtime.getState('run-shadow-1');
|
||||
assert.deepEqual(restored, state);
|
||||
|
||||
const eventPage = await runtime.listEvents('run-shadow-1');
|
||||
assert.deepEqual(
|
||||
eventPage.events.map((event) => event.type),
|
||||
['workflow_validated', 'workflow_planned', 'workflow_completed'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
eventPage.events.map((event) => event.sequence),
|
||||
[1, 2, 3],
|
||||
);
|
||||
assert.equal(eventPage.nextCursor, 3);
|
||||
assert.equal((await runtime.listEvents('run-shadow-1', { after: 2 })).events.length, 1);
|
||||
});
|
||||
|
||||
test('LangGraph runtime is idempotent by run id', async () => {
|
||||
const runtime = createLangGraphOrchestratorRuntime({
|
||||
checkpointer: new MemorySaver(),
|
||||
});
|
||||
|
||||
const first = await runtime.start(runSpec());
|
||||
const second = await runtime.start(runSpec({
|
||||
input: { instruction: 'This must not replace the first checkpoint' },
|
||||
}));
|
||||
|
||||
assert.deepEqual(second, first);
|
||||
assert.equal((await runtime.listEvents('run-shadow-1')).events.length, 3);
|
||||
});
|
||||
|
||||
test('LangGraph runtime rejects execution-enabled and unsupported workflows', async () => {
|
||||
const runtime = createLangGraphOrchestratorRuntime({
|
||||
checkpointer: new MemorySaver(),
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
runtime.start(runSpec({
|
||||
runId: 'run-active',
|
||||
policy: { executionMode: 'active', sideEffectsAllowed: true },
|
||||
})),
|
||||
(error) => error.code === 'WORKFLOW_OBSERVE_ONLY_REQUIRED',
|
||||
);
|
||||
await assert.rejects(
|
||||
runtime.start(runSpec({
|
||||
runId: 'run-unsupported',
|
||||
workflow: { name: 'research-v1', version: 1 },
|
||||
})),
|
||||
(error) => error.code === 'WORKFLOW_NOT_SUPPORTED' && error.status === 422,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { createOrchestratorApp } from './app.mjs';
|
||||
import { createOrchestratorCheckpoint } from './checkpoint.mjs';
|
||||
import { createLangGraphOrchestratorRuntime } from './runtime.mjs';
|
||||
|
||||
function positivePort(value, fallback = 8093) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) return fallback;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export async function startOrchestratorServer({
|
||||
env = process.env,
|
||||
logger = console,
|
||||
} = {}) {
|
||||
const checkpoint = await createOrchestratorCheckpoint({
|
||||
mode: env.MEMIND_ORCHESTRATOR_CHECKPOINT_MODE,
|
||||
connectionString: env.MEMIND_ORCHESTRATOR_DATABASE_URL,
|
||||
schema: env.MEMIND_ORCHESTRATOR_DATABASE_SCHEMA,
|
||||
});
|
||||
const runtime = createLangGraphOrchestratorRuntime({
|
||||
checkpointer: checkpoint.checkpointer,
|
||||
checkpointKind: checkpoint.kind,
|
||||
durable: checkpoint.durable,
|
||||
checkpointProbe: checkpoint.probe,
|
||||
});
|
||||
const app = createOrchestratorApp({
|
||||
runtime,
|
||||
serviceToken: env.MEMIND_ORCHESTRATOR_SERVICE_TOKEN,
|
||||
});
|
||||
const host = String(env.MEMIND_ORCHESTRATOR_HOST ?? '127.0.0.1').trim() || '127.0.0.1';
|
||||
const port = positivePort(env.MEMIND_ORCHESTRATOR_PORT, 8093);
|
||||
const server = await new Promise((resolve, reject) => {
|
||||
const listening = app.listen(port, host, () => resolve(listening));
|
||||
listening.once('error', reject);
|
||||
});
|
||||
logger.log(`[orchestrator] listening on http://${host}:${port} (${checkpoint.kind})`);
|
||||
|
||||
async function close() {
|
||||
await new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
await checkpoint.close();
|
||||
}
|
||||
|
||||
return { app, server, runtime, checkpoint, close };
|
||||
}
|
||||
|
||||
const isEntrypoint = process.argv[1]
|
||||
&& import.meta.url === pathToFileURL(process.argv[1]).href;
|
||||
|
||||
if (isEntrypoint) {
|
||||
const running = await startOrchestratorServer();
|
||||
const shutdown = async (signal) => {
|
||||
console.log(`[orchestrator] received ${signal}, shutting down`);
|
||||
await running.close();
|
||||
process.exit(0);
|
||||
};
|
||||
process.once('SIGINT', () => void shutdown('SIGINT'));
|
||||
process.once('SIGTERM', () => void shutdown('SIGTERM'));
|
||||
}
|
||||
|
||||
export const orchestratorServerInternals = {
|
||||
positivePort,
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
WORKFLOW_ENGINE,
|
||||
normalizeRunSpec,
|
||||
} from './contracts.mjs';
|
||||
import { createRemoteWorkflowEngine } from './engine-registry.mjs';
|
||||
|
||||
const MAX_INSTRUCTION_CHARACTERS = 16_000;
|
||||
|
||||
function extractMessageText(message) {
|
||||
if (typeof message === 'string') return message;
|
||||
if (!message || typeof message !== 'object') return '';
|
||||
if (typeof message.content === 'string') return message.content;
|
||||
if (!Array.isArray(message.content)) return '';
|
||||
return message.content
|
||||
.filter((part) => part?.type === 'text')
|
||||
.map((part) => String(part.text ?? ''))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function safeError(error) {
|
||||
return {
|
||||
code: String(error?.code ?? 'WORKFLOW_SHADOW_FAILED').slice(0, 128),
|
||||
message: String(error instanceof Error ? error.message : error).slice(0, 1000),
|
||||
};
|
||||
}
|
||||
|
||||
export function createWorkflowShadowObserver({
|
||||
configService,
|
||||
serviceToken = process.env.MEMIND_ORCHESTRATOR_SERVICE_TOKEN,
|
||||
fetchImpl = globalThis.fetch,
|
||||
logger = console,
|
||||
} = {}) {
|
||||
if (!configService?.selectEngine || !configService?.getRuntimeState) {
|
||||
throw new Error('Workflow shadow observer requires orchestrator config service');
|
||||
}
|
||||
|
||||
return async function observeWorkflowRun({
|
||||
runId,
|
||||
requestId,
|
||||
userId,
|
||||
sessionId = null,
|
||||
workflowName = 'code-run-v1',
|
||||
userMessage,
|
||||
taskType = null,
|
||||
} = {}) {
|
||||
const selection = await configService.selectEngine({
|
||||
runId,
|
||||
requestId,
|
||||
userId,
|
||||
workflowName,
|
||||
});
|
||||
if (selection.shadowEngine !== WORKFLOW_ENGINE.LANGGRAPH) {
|
||||
return {
|
||||
observed: false,
|
||||
reason: selection.reason,
|
||||
mode: selection.mode,
|
||||
};
|
||||
}
|
||||
|
||||
const state = await configService.getRuntimeState();
|
||||
const engine = createRemoteWorkflowEngine({
|
||||
id: WORKFLOW_ENGINE.LANGGRAPH,
|
||||
baseUrl: state.config.serviceUrl,
|
||||
serviceToken,
|
||||
timeoutMs: state.config.requestTimeoutMs,
|
||||
fetchImpl,
|
||||
});
|
||||
const instruction = extractMessageText(userMessage).slice(0, MAX_INSTRUCTION_CHARACTERS);
|
||||
const spec = normalizeRunSpec({
|
||||
runId,
|
||||
requestId,
|
||||
workflow: { name: workflowName, version: 1 },
|
||||
subject: { userId },
|
||||
input: {
|
||||
instruction,
|
||||
taskType,
|
||||
toolMode: 'code',
|
||||
sessionRef: sessionId
|
||||
? { kind: 'goose-session', id: String(sessionId) }
|
||||
: null,
|
||||
},
|
||||
policy: {
|
||||
executionMode: 'observe-only',
|
||||
sideEffectsAllowed: false,
|
||||
},
|
||||
metadata: {
|
||||
source: 'memind-agent-run',
|
||||
configVersion: selection.configVersion,
|
||||
},
|
||||
});
|
||||
try {
|
||||
const result = await engine.start(spec);
|
||||
return {
|
||||
observed: true,
|
||||
engine: WORKFLOW_ENGINE.LANGGRAPH,
|
||||
mode: selection.mode,
|
||||
configVersion: selection.configVersion,
|
||||
shadowRun: result,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.warn('[orchestrator-shadow] observation failed:', safeError(error));
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const workflowShadowObserverInternals = {
|
||||
MAX_INSTRUCTION_CHARACTERS,
|
||||
extractMessageText,
|
||||
safeError,
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createWorkflowShadowObserver } from './shadow-observer.mjs';
|
||||
|
||||
function jsonResponse(body, status = 200) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
test('shadow observer skips without creating a remote client when mode does not select shadow', async () => {
|
||||
let fetchCalls = 0;
|
||||
const observer = createWorkflowShadowObserver({
|
||||
configService: {
|
||||
async selectEngine() {
|
||||
return { shadowEngine: null, reason: 'mode_off', mode: 'off' };
|
||||
},
|
||||
async getRuntimeState() {
|
||||
throw new Error('must not load runtime config');
|
||||
},
|
||||
},
|
||||
fetchImpl: async () => {
|
||||
fetchCalls += 1;
|
||||
return jsonResponse({});
|
||||
},
|
||||
});
|
||||
|
||||
const result = await observer({
|
||||
runId: 'run-1',
|
||||
requestId: 'request-1',
|
||||
userId: 'user-1',
|
||||
});
|
||||
assert.deepEqual(result, {
|
||||
observed: false,
|
||||
reason: 'mode_off',
|
||||
mode: 'off',
|
||||
});
|
||||
assert.equal(fetchCalls, 0);
|
||||
});
|
||||
|
||||
test('shadow observer sends a bounded observe-only RunSpec to LangGraph service', async () => {
|
||||
let capturedUrl = null;
|
||||
let capturedInit = null;
|
||||
const observer = createWorkflowShadowObserver({
|
||||
configService: {
|
||||
async selectEngine() {
|
||||
return {
|
||||
shadowEngine: 'langgraph',
|
||||
reason: 'shadow',
|
||||
mode: 'shadow',
|
||||
configVersion: 7,
|
||||
};
|
||||
},
|
||||
async getRuntimeState() {
|
||||
return {
|
||||
config: {
|
||||
serviceUrl: 'http://orchestrator.internal:8093',
|
||||
requestTimeoutMs: 1200,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
serviceToken: 'internal-token',
|
||||
fetchImpl: async (url, init) => {
|
||||
capturedUrl = url;
|
||||
capturedInit = init;
|
||||
return jsonResponse({ runId: 'run-2', status: 'succeeded' }, 202);
|
||||
},
|
||||
});
|
||||
|
||||
const result = await observer({
|
||||
runId: 'run-2',
|
||||
requestId: 'request-2',
|
||||
userId: 'user-2',
|
||||
sessionId: 'session-2',
|
||||
taskType: 'code-change',
|
||||
userMessage: {
|
||||
content: [{ type: 'text', text: 'Implement the service boundary' }],
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.observed, true);
|
||||
assert.equal(capturedUrl, 'http://orchestrator.internal:8093/v1/runs');
|
||||
assert.equal(capturedInit.headers.authorization, 'Bearer internal-token');
|
||||
const body = JSON.parse(capturedInit.body);
|
||||
assert.equal(body.version, 'orchestrator-run-v1');
|
||||
assert.equal(body.policy.executionMode, 'observe-only');
|
||||
assert.equal(body.policy.sideEffectsAllowed, false);
|
||||
assert.deepEqual(body.input.sessionRef, { kind: 'goose-session', id: 'session-2' });
|
||||
assert.equal(body.metadata.configVersion, 7);
|
||||
});
|
||||
Reference in New Issue
Block a user