@sema-agent/server 7.67.0 → 7.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/USAGE.md +19 -2
- package/dist/adoption/plan.js +11 -0
- package/dist/approval-card.d.ts +26 -0
- package/dist/approval-card.js +11 -0
- package/dist/approval.js +5 -4
- package/dist/boot/config-center.d.ts +6 -7
- package/dist/boot/config-center.js +31 -8
- package/dist/boot/leader.js +1 -1
- package/dist/boot/resolve-spec.js +1 -1
- package/dist/boot/workflow-orchestration.d.ts +2 -0
- package/dist/boot/workflow-orchestration.js +10 -2
- package/dist/budget.js +1 -1
- package/dist/config-catalog.js +20 -1
- package/dist/config-center/apply-effective.d.ts +8 -1
- package/dist/config-center/apply-effective.js +23 -9
- package/dist/config-center/facade.d.ts +1 -1
- package/dist/config-center/facade.js +1 -1
- package/dist/config-center/read-face.d.ts +30 -1
- package/dist/config-center/read-face.js +33 -15
- package/dist/config-center/restart-signal.js +2 -1
- package/dist/config-center/types.d.ts +20 -5
- package/dist/config-provider.js +1 -0
- package/dist/config-types.d.ts +17 -2
- package/dist/config.d.ts +0 -21
- package/dist/config.js +19 -6
- package/dist/http/route-ctx.d.ts +44 -2
- package/dist/http/routes/approvals-assistant.js +1 -0
- package/dist/http/routes/capabilities.js +1 -0
- package/dist/http/routes/runs.js +1 -1
- package/dist/http/routes/tasks.js +1 -0
- package/dist/http/routes/workflows.js +15 -7
- package/dist/http/server.d.ts +15 -2
- package/dist/http/server.js +74 -45
- package/dist/leader/wire.d.ts +9 -2
- package/dist/leader/wire.js +6 -6
- package/dist/main.js +6 -5
- package/dist/mcp-tool-faces.js +11 -2
- package/dist/model-compat.d.ts +81 -0
- package/dist/model-compat.js +52 -0
- package/dist/observability/fail-open.d.ts +4 -0
- package/dist/observability/fail-open.js +4 -0
- package/dist/operator-ask.d.ts +46 -0
- package/dist/operator-ask.js +4 -0
- package/dist/orchestration/workflow-agent-session-index.d.ts +96 -0
- package/dist/orchestration/workflow-agent-session-index.js +146 -0
- package/dist/orchestration/workflow-journal-entry.d.ts +78 -0
- package/dist/orchestration/workflow-journal-entry.js +20 -0
- package/dist/orchestration/workflow-notify-journal.d.ts +43 -1
- package/dist/orchestration/workflow-notify-journal.js +43 -3
- package/dist/parked-decide.d.ts +115 -5
- package/dist/parked-decide.js +94 -26
- package/dist/plugins/pg-pool.js +8 -0
- package/dist/plugins/store-backend.d.ts +5 -0
- package/dist/plugins/store-backend.js +3 -1
- package/dist/plugins/tidb-pool.js +8 -0
- package/dist/plugins/workflow-journal-store-sql.js +10 -3
- package/dist/plugins/workflow-run-store-sql.d.ts +30 -0
- package/dist/plugins/workflow-run-store-sql.js +40 -0
- package/dist/router/route-orchestration.js +2 -1
- package/dist/run-local.js +1 -1
- package/dist/runs.d.ts +10 -1
- package/dist/runs.js +4 -1
- package/dist/runtime-governance.js +5 -4
- package/dist/task-mcp.d.ts +50 -6
- package/dist/task-mcp.js +71 -35
- package/dist/tool-approval.d.ts +20 -0
- package/dist/tool-approval.js +2 -1
- package/dist/trace/core-keyset-guard.d.ts +3 -3
- package/dist/trace/engine-notice-wire.d.ts +17 -1
- package/dist/trace/engine-notice-wire.js +7 -1
- package/package.json +4 -4
package/dist/parked-decide.js
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
import { defaultSubagentTailBus } from "./fleet/subagent-tail-bus.js";
|
|
2
2
|
import { backgroundScopesForCheckpointScope, decodeCheckpointScope } from "./security.js";
|
|
3
|
+
export async function findParkOrigin(sources, cp) {
|
|
4
|
+
if (sources.agentStore !== undefined) {
|
|
5
|
+
let match;
|
|
6
|
+
try {
|
|
7
|
+
match = await findParkedAgentForCheckpoint(sources.agentStore, cp);
|
|
8
|
+
}
|
|
9
|
+
catch (e) {
|
|
10
|
+
quiet(sources.warn, "parked_decide_probe_failed", { error: e instanceof Error ? e.message : String(e) });
|
|
11
|
+
}
|
|
12
|
+
if (match !== undefined)
|
|
13
|
+
return { kind: "background", handle: match.handle, row: match.row };
|
|
14
|
+
}
|
|
15
|
+
const index = sources.workflowIndex;
|
|
16
|
+
const runStore = sources.workflowRunStore;
|
|
17
|
+
if (index === undefined || runStore === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
try {
|
|
20
|
+
const row = await index.findByAgentSession(cp.sessionId);
|
|
21
|
+
if (row === null)
|
|
22
|
+
return undefined;
|
|
23
|
+
const run = await runStore.get(row.runId);
|
|
24
|
+
if (run === null)
|
|
25
|
+
return undefined;
|
|
26
|
+
const agent = (run.agents ?? []).find((a) => a.sessionId === cp.sessionId && a.status === "parked");
|
|
27
|
+
if (agent === undefined || agent.parkedCheckpointToken !== cp.token)
|
|
28
|
+
return undefined;
|
|
29
|
+
return { kind: "workflow", runId: row.runId, run, agent };
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
quiet(sources.warn, "workflow_park_probe_failed", { error: e instanceof Error ? e.message : String(e) });
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function quiet(warn, event, fields) {
|
|
37
|
+
try {
|
|
38
|
+
warn?.(event, fields);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
3
43
|
export async function findParkedAgentForCheckpoint(agentStore, cp) {
|
|
4
44
|
for (const scope of backgroundScopesForCheckpointScope(cp.scope)) {
|
|
5
45
|
const parked = await agentStore.listByScope(scope, { status: "parked" });
|
|
@@ -25,21 +65,61 @@ function mintReviveArgs(row, knownAgentTypes) {
|
|
|
25
65
|
...(row.agentType !== undefined && knownAgentTypes.has(row.agentType) ? { subagent_type: row.agentType } : {}),
|
|
26
66
|
};
|
|
27
67
|
}
|
|
28
|
-
export
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
68
|
+
export function buildParkedResumeOutcome(req) {
|
|
69
|
+
const pa = req.pendingAction;
|
|
70
|
+
const persistedCallId = pa?.kind === "tool_approval" ? (pa.toolCallId ?? "") : "";
|
|
71
|
+
const persistedHash = pa?.kind === "tool_approval" ? (pa.boundInputHash ?? "") : "";
|
|
72
|
+
return {
|
|
73
|
+
gate: "policy_ask",
|
|
74
|
+
boundCallId: req.binding?.boundCallId ?? persistedCallId,
|
|
75
|
+
boundInputHash: req.binding?.boundInputHash ?? persistedHash,
|
|
76
|
+
decision: req.decision === "approve" ? "allow" : "deny",
|
|
77
|
+
...(req.decision === "approve" && req.binding?.updatedInput !== undefined ? { updatedInput: req.binding.updatedInput } : {}),
|
|
78
|
+
...(req.answer !== undefined ? { answer: req.answer } : {}),
|
|
79
|
+
...(req.reason !== undefined ? { reason: req.reason } : {}),
|
|
80
|
+
hostDecision: req.hostDecision,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function planWorkflowParkedDecide(origin, req) {
|
|
84
|
+
if (req.grantRemember !== undefined) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
deny: {
|
|
88
|
+
status: 400,
|
|
89
|
+
body: {
|
|
90
|
+
error: "remember:\"session\" is not supported for a parked WORKFLOW agent: this lane's 200 means the decision was handed to the host's next turn, not that the engine committed it — an exemption granted here could outlive a decision the engine later rejects. Re-issue the decide without `remember`.",
|
|
91
|
+
errorCode: "decide.workflow_remember_unsupported",
|
|
92
|
+
runId: origin.runId,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
};
|
|
32
96
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
97
|
+
const hostSessionId = origin.run.originatingSessionId;
|
|
98
|
+
if (hostSessionId === undefined || hostSessionId === "") {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
deny: {
|
|
102
|
+
status: 409,
|
|
103
|
+
body: {
|
|
104
|
+
error: "this parked workflow agent's run carries no originating session — the deployment has nowhere to deliver the decision (a directly-started workflow, not a Workflow tool call)",
|
|
105
|
+
errorCode: "decide.workflow_host_unknown",
|
|
106
|
+
runId: origin.runId,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
};
|
|
40
110
|
}
|
|
41
|
-
|
|
42
|
-
|
|
111
|
+
const verdict = req.decision === "approve" ? "APPROVED" : "DENIED";
|
|
112
|
+
return {
|
|
113
|
+
ok: true,
|
|
114
|
+
hostSessionId,
|
|
115
|
+
runId: origin.runId,
|
|
116
|
+
outcome: buildParkedResumeOutcome(req),
|
|
117
|
+
message: `The human approval that workflow run "${origin.runId}" parked on (agent callKey "${origin.agent.callKey}") has been ${verdict}. ` +
|
|
118
|
+
`Resume that run now by calling Workflow with resumeFromRunId: "${origin.runId}" — the decision is already held by the deployment ` +
|
|
119
|
+
`and will be applied to the parked agent; completed steps replay from the journal. Do not re-issue the original call.`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
export async function decideParkedAgent(deps, req, match) {
|
|
43
123
|
const row = match.row;
|
|
44
124
|
const rememberRoot = req.grantRemember !== undefined ? (row.rootSessionId ?? row.parentSessionId) : undefined;
|
|
45
125
|
if (req.grantRemember !== undefined && rememberRoot === undefined) {
|
|
@@ -81,19 +161,7 @@ export async function decideParkedAgent(deps, req) {
|
|
|
81
161
|
catch {
|
|
82
162
|
}
|
|
83
163
|
}
|
|
84
|
-
const
|
|
85
|
-
const persistedCallId = pa?.kind === "tool_approval" ? (pa.toolCallId ?? "") : "";
|
|
86
|
-
const persistedHash = pa?.kind === "tool_approval" ? (pa.boundInputHash ?? "") : "";
|
|
87
|
-
const outcome = {
|
|
88
|
-
gate: "policy_ask",
|
|
89
|
-
boundCallId: req.binding?.boundCallId ?? persistedCallId,
|
|
90
|
-
boundInputHash: req.binding?.boundInputHash ?? persistedHash,
|
|
91
|
-
decision: req.decision === "approve" ? "allow" : "deny",
|
|
92
|
-
...(req.decision === "approve" && req.binding?.updatedInput !== undefined ? { updatedInput: req.binding.updatedInput } : {}),
|
|
93
|
-
...(req.answer !== undefined ? { answer: req.answer } : {}),
|
|
94
|
-
...(req.reason !== undefined ? { reason: req.reason } : {}),
|
|
95
|
-
hostDecision: req.hostDecision,
|
|
96
|
-
};
|
|
164
|
+
const outcome = buildParkedResumeOutcome(req);
|
|
97
165
|
const ctx = {
|
|
98
166
|
toolCallId: `drv-${ticket.claimId}`,
|
|
99
167
|
reviveClaim: {
|
package/dist/plugins/pg-pool.js
CHANGED
|
@@ -266,6 +266,14 @@ export const PG_SCHEMA_STATEMENTS = [
|
|
|
266
266
|
)`,
|
|
267
267
|
`CREATE INDEX IF NOT EXISTS idx_wfrun_scope_created ON workflow_run (scope, created_at_ms)`,
|
|
268
268
|
`CREATE INDEX IF NOT EXISTS idx_wfrun_scope_status ON workflow_run (scope, status)`,
|
|
269
|
+
`CREATE TABLE IF NOT EXISTS workflow_agent_session (
|
|
270
|
+
session_id VARCHAR(190) COLLATE "C" NOT NULL,
|
|
271
|
+
run_id VARCHAR(191) COLLATE "C" NOT NULL,
|
|
272
|
+
agent_key VARCHAR(191) COLLATE "C" NOT NULL,
|
|
273
|
+
scope VARCHAR(190) COLLATE "C" NOT NULL,
|
|
274
|
+
PRIMARY KEY (session_id)
|
|
275
|
+
)`,
|
|
276
|
+
`CREATE INDEX IF NOT EXISTS idx_wfagentsess_run ON workflow_agent_session (run_id)`,
|
|
269
277
|
`CREATE TABLE IF NOT EXISTS workflow_completion_inbox (
|
|
270
278
|
seq BIGSERIAL,
|
|
271
279
|
session_id VARCHAR(190) COLLATE "C" NOT NULL,
|
|
@@ -238,6 +238,11 @@ export interface StoreBackend {
|
|
|
238
238
|
/** 1.108 review fix (lens③ HIGH): the notify-JOURNAL twin — third leg of the same axis (a SQL run store +
|
|
239
239
|
* inbox with a replica-LOCAL File journal stranded a dead replica's un-acked notify forever). */
|
|
240
240
|
notifyJournal?(): import("../orchestration/workflow-notify-journal.js").WorkflowNotifyJournalStore;
|
|
241
|
+
/** S-185 车CM:workflow 出身 park 的**有界 join** 索引(子会话 id → 它所属的 workflow run)。
|
|
242
|
+
* 与 `workflowRun`/`completionInbox`/`notifyJournal` 同一条轴 —— SQL 后端 = 跨副本双生;`local` 车道
|
|
243
|
+
* 缺席**是一个真答案**:那里的索引家在 run 店同一间屋子里(`LocalWorkflowAgentSessionIndex`,文件账本
|
|
244
|
+
* 或纯内存),不需要经过 backend。契约与单写者纪律的属主 = orchestration/workflow-agent-session-index.ts。 */
|
|
245
|
+
workflowAgentSessionIndex?(): import("../orchestration/workflow-agent-session-index.js").WorkflowAgentSessionIndex;
|
|
241
246
|
/** Cloud-only stores below are OPTIONAL: the `local` (in-memory/file) backend omits them (it has no core
|
|
242
247
|
* in-memory twin carrying the service extras, and these are fleet/multi-replica concepts), so a local HTTP
|
|
243
248
|
* service degrades EXACTLY like today's no-DB env (the consumers gate on `backend?.X?.()`). TiDB/PG provide all. */
|
|
@@ -4,7 +4,7 @@ import { AdoptionError, FileStorageBackend, FileSessionRepo } from "@sema-agent/
|
|
|
4
4
|
import { FileWorkflowJournalStore } from "@sema-agent/core";
|
|
5
5
|
import { TiDBOutcomeLedger, PgOutcomeLedger } from "./outcome-ledger-sql.js";
|
|
6
6
|
import { FileOutcomeSink } from "./file-outcome-sink.js";
|
|
7
|
-
import { TiDBWorkflowRunStore, TiDBWorkflowCompletionInbox, TiDBWorkflowNotifyJournalStore, PgWorkflowRunStore, PgWorkflowCompletionInbox, PgWorkflowNotifyJournalStore } from "./workflow-run-store-sql.js";
|
|
7
|
+
import { TiDBWorkflowRunStore, TiDBWorkflowCompletionInbox, TiDBWorkflowNotifyJournalStore, TiDBWorkflowAgentSessionIndex, PgWorkflowRunStore, PgWorkflowCompletionInbox, PgWorkflowNotifyJournalStore, PgWorkflowAgentSessionIndex } from "./workflow-run-store-sql.js";
|
|
8
8
|
import { FileRunStore, RunStoreStrictHydrateError } from "./file-run-store.js";
|
|
9
9
|
import { LocalSessionStore } from "./local-session-store.js";
|
|
10
10
|
import { LocalCheckpointStore } from "./local-checkpoint-store.js";
|
|
@@ -145,6 +145,7 @@ class TiDBBackend {
|
|
|
145
145
|
workflowRun(onWarn) { return new TiDBWorkflowRunStore(this.pool, onWarn); }
|
|
146
146
|
completionInbox(onWarn) { return new TiDBWorkflowCompletionInbox(this.pool, onWarn); }
|
|
147
147
|
notifyJournal() { return new TiDBWorkflowNotifyJournalStore(this.pool); }
|
|
148
|
+
workflowAgentSessionIndex() { return new TiDBWorkflowAgentSessionIndex(this.pool); }
|
|
148
149
|
sharedMemoryStore(authorizer) { return new TiDBSharedMemoryStore(this.pool, authorizer); }
|
|
149
150
|
checkpoint(logger) { return new TiDBCheckpointStore(this.pool, logger); }
|
|
150
151
|
toolResult() { return new TiDBToolResultStore(this.pool); }
|
|
@@ -191,6 +192,7 @@ class PgBackend {
|
|
|
191
192
|
workflowRun(onWarn) { return new PgWorkflowRunStore(this.pool, onWarn); }
|
|
192
193
|
completionInbox(onWarn) { return new PgWorkflowCompletionInbox(this.pool, onWarn); }
|
|
193
194
|
notifyJournal() { return new PgWorkflowNotifyJournalStore(this.pool); }
|
|
195
|
+
workflowAgentSessionIndex() { return new PgWorkflowAgentSessionIndex(this.pool); }
|
|
194
196
|
sharedMemoryStore(authorizer) { return new PgSharedMemoryStore(this.pool, authorizer); }
|
|
195
197
|
checkpoint(logger) { return new PgCheckpointStore(this.pool, logger); }
|
|
196
198
|
toolResult() { return new PgToolResultStore(this.pool); }
|
|
@@ -446,6 +446,14 @@ export const SCHEMA_STATEMENTS = [
|
|
|
446
446
|
PRIMARY KEY (id),
|
|
447
447
|
KEY idx_wfrun_scope_created (scope, created_at_ms),
|
|
448
448
|
KEY idx_wfrun_scope_status (scope, status)
|
|
449
|
+
) COLLATE utf8mb4_bin`,
|
|
450
|
+
`CREATE TABLE IF NOT EXISTS workflow_agent_session (
|
|
451
|
+
session_id VARCHAR(190) NOT NULL,
|
|
452
|
+
run_id VARCHAR(191) NOT NULL,
|
|
453
|
+
agent_key VARCHAR(191) NOT NULL,
|
|
454
|
+
scope VARCHAR(190) NOT NULL,
|
|
455
|
+
PRIMARY KEY (session_id),
|
|
456
|
+
KEY idx_wfagentsess_run (run_id)
|
|
449
457
|
) COLLATE utf8mb4_bin`,
|
|
450
458
|
`CREATE TABLE IF NOT EXISTS workflow_completion_inbox (
|
|
451
459
|
seq BIGINT NOT NULL AUTO_INCREMENT,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { callKeyOrdinal } from "@sema-agent/core";
|
|
2
2
|
import { oversizeJournalResult } from "@sema-agent/core";
|
|
3
|
+
import { journalEntryOfRow, journalEntryPayload, journalRowPayloadOf } from "../orchestration/workflow-journal-entry.js";
|
|
3
4
|
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
4
5
|
const DEFAULT_RESUME_CLAIM_TTL_MS = 60 * 60 * 1000;
|
|
5
6
|
export class SqlWorkflowJournalStore {
|
|
@@ -13,16 +14,22 @@ export class SqlWorkflowJournalStore {
|
|
|
13
14
|
return this.db.dialect === "tidb" ? tidb : pg;
|
|
14
15
|
}
|
|
15
16
|
async append(runId, scope, entry) {
|
|
16
|
-
const serialized =
|
|
17
|
-
if (oversizeJournalResult(serialized))
|
|
17
|
+
const serialized = journalRowPayloadOf(entry);
|
|
18
|
+
if (oversizeJournalResult(serialized)) {
|
|
19
|
+
if (journalEntryPayload(entry).parked) {
|
|
20
|
+
throw new Error(`workflow journal: PARKED entry ${entry.callKey} is ${serialized.length} bytes, over the journal result cap — refusing to skip it. ` +
|
|
21
|
+
"Skipping a parked entry would make a resume start a SECOND live agent beside the session already pinned by the pending checkpoint (duplicating pre-approval side effects); " +
|
|
22
|
+
"a completed result may be skipped (resume just re-runs that agent), a park may not.");
|
|
23
|
+
}
|
|
18
24
|
return;
|
|
25
|
+
}
|
|
19
26
|
await this.db.query(this.q("INSERT INTO workflow_journal (run_id, ordinal, scope, call_key, result, created_at_ms) VALUES (?,?,?,?,?,?) " +
|
|
20
27
|
"ON DUPLICATE KEY UPDATE call_key = VALUES(call_key), result = VALUES(result)", "INSERT INTO workflow_journal (run_id, ordinal, scope, call_key, result, created_at_ms) VALUES ($1,$2,$3,$4,$5,$6) " +
|
|
21
28
|
"ON CONFLICT (run_id, ordinal) DO UPDATE SET call_key = EXCLUDED.call_key, result = EXCLUDED.result"), [runId, callKeyOrdinal(entry.callKey), scope, entry.callKey, serialized, Date.now()]);
|
|
22
29
|
}
|
|
23
30
|
async load(runId, scope) {
|
|
24
31
|
const { rows } = await this.db.query(this.q("SELECT call_key, result FROM workflow_journal WHERE run_id = ? AND scope = ? ORDER BY ordinal ASC", "SELECT call_key, result FROM workflow_journal WHERE run_id = $1 AND scope = $2 ORDER BY ordinal ASC"), [runId, scope]);
|
|
25
|
-
return rows.map((r) => (
|
|
32
|
+
return rows.map((r) => journalEntryOfRow(String(r.call_key), String(r.result)));
|
|
26
33
|
}
|
|
27
34
|
locator(runId, _scope) {
|
|
28
35
|
return `/v1/workflows/${runId}/journal`;
|
|
@@ -59,6 +59,7 @@ import type { Pool as PgPool } from "pg";
|
|
|
59
59
|
import { summarizeWorkflowRun, type WorkflowRunStore } from "@sema-agent/core";
|
|
60
60
|
import { type WorkflowCompletionInbox, type WorkflowCompletionInboxEntry } from "../orchestration/workflow-completion-inbox.js";
|
|
61
61
|
import type { WorkflowNotifyJournalStore, WorkflowNotifyJournalEntry } from "../orchestration/workflow-notify-journal.js";
|
|
62
|
+
import type { WorkflowAgentSessionIndex, WorkflowAgentSessionRow } from "../orchestration/workflow-agent-session-index.js";
|
|
62
63
|
import { type SqlDriver } from "./sql-driver.js";
|
|
63
64
|
type WorkflowRun = Parameters<WorkflowRunStore["put"]>[1];
|
|
64
65
|
/** Row cap guard: a run blob beyond this is NOT stored on update (the prior revision stays) — TiDB's
|
|
@@ -153,6 +154,29 @@ export declare class SqlWorkflowNotifyJournalStore implements WorkflowNotifyJour
|
|
|
153
154
|
reapAcked(before: number): Promise<number>;
|
|
154
155
|
get(runId: string): Promise<WorkflowNotifyJournalEntry | null>;
|
|
155
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* S-185 车CM —— **workflow 出身 park 的有界 join 索引** SQL 双生(契约与纪律的唯一属主 =
|
|
159
|
+
* {@link import("../orchestration/workflow-agent-session-index.js").WorkflowAgentSessionIndex} 的文件头,
|
|
160
|
+
* 这里只记方言差)。行是 `WorkflowRun` 的派生投影,写者只有一个(Journaling 装饰器的写观察点)。
|
|
161
|
+
*
|
|
162
|
+
* ── 方言差,保持显式 ──────────────────────────────────────────────────────────────────────────
|
|
163
|
+
* - `?` 占位 vs `$n`,且 `syncRun` 的删除腿在 PG 上要按 `params.length` 记位(与 `listByScope` 同因)。
|
|
164
|
+
* - 「删掉这条 run 里已不在 rows 里的旧行」:TiDB `session_id NOT IN (?,?,…)`(动态占位数)vs
|
|
165
|
+
* PG `session_id <> ALL($n::text[])`(数组绑定)—— 两边同一件事,语句结构不同(reap 的批删同先例)。
|
|
166
|
+
* - upsert:TiDB `REPLACE INTO`(整行替换)vs PG `INSERT … ON CONFLICT (session_id) DO UPDATE`。
|
|
167
|
+
* - schema 属主同店内其余表:TiDB DDL 在 tidb-pool.ts,PG DDL 在 pg-pool.ts(店不建表)。
|
|
168
|
+
*
|
|
169
|
+
* 🔴 **写序**:先 upsert 再删。反过来(先删后 upsert)会在两条语句之间留出一个**索引行不存在**的窗口,
|
|
170
|
+
* 而这期间进来的 `/decide` 会诚实 miss ⇒ 一次本可受理的批准被退回。先 upsert 的最坏形是窗口内多一条
|
|
171
|
+
* 旧行(读口按 session_id 精确查,取 run 后还要逐字比对赎回键 ⇒ 多余的行只会 miss,不会错配)。
|
|
172
|
+
*/
|
|
173
|
+
export declare class SqlWorkflowAgentSessionIndex implements WorkflowAgentSessionIndex {
|
|
174
|
+
private readonly db;
|
|
175
|
+
constructor(db: SqlDriver);
|
|
176
|
+
private q;
|
|
177
|
+
syncRun(runId: string, rows: readonly WorkflowAgentSessionRow[]): Promise<void>;
|
|
178
|
+
findByAgentSession(sessionId: string): Promise<WorkflowAgentSessionRow | null>;
|
|
179
|
+
}
|
|
156
180
|
/** MySQL-protocol (TiDB) bindings — historical class names preserved; `onWarn` is an ADDITIVE optional 2nd
|
|
157
181
|
* ctor arg (existing 1-arg call sites are unaffected — see {@link SqlWorkflowRunStore}'s C5 trace channel). */
|
|
158
182
|
export declare class TiDBWorkflowRunStore extends SqlWorkflowRunStore {
|
|
@@ -175,5 +199,11 @@ export declare class PgWorkflowCompletionInbox extends SqlWorkflowCompletionInbo
|
|
|
175
199
|
export declare class PgWorkflowNotifyJournalStore extends SqlWorkflowNotifyJournalStore {
|
|
176
200
|
constructor(pool: PgPool);
|
|
177
201
|
}
|
|
202
|
+
export declare class TiDBWorkflowAgentSessionIndex extends SqlWorkflowAgentSessionIndex {
|
|
203
|
+
constructor(pool: MySqlPool);
|
|
204
|
+
}
|
|
205
|
+
export declare class PgWorkflowAgentSessionIndex extends SqlWorkflowAgentSessionIndex {
|
|
206
|
+
constructor(pool: PgPool);
|
|
207
|
+
}
|
|
178
208
|
export {};
|
|
179
209
|
//# sourceMappingURL=workflow-run-store-sql.d.ts.map
|
|
@@ -277,6 +277,36 @@ export class SqlWorkflowNotifyJournalStore {
|
|
|
277
277
|
};
|
|
278
278
|
}
|
|
279
279
|
}
|
|
280
|
+
export class SqlWorkflowAgentSessionIndex {
|
|
281
|
+
db;
|
|
282
|
+
constructor(db) {
|
|
283
|
+
this.db = db;
|
|
284
|
+
}
|
|
285
|
+
q(tidb, pg) {
|
|
286
|
+
return this.db.dialect === "tidb" ? tidb : pg;
|
|
287
|
+
}
|
|
288
|
+
async syncRun(runId, rows) {
|
|
289
|
+
for (const r of rows) {
|
|
290
|
+
await this.db.query(this.q("REPLACE INTO workflow_agent_session (session_id, run_id, agent_key, scope) VALUES (?,?,?,?)", `INSERT INTO workflow_agent_session (session_id, run_id, agent_key, scope) VALUES ($1,$2,$3,$4)
|
|
291
|
+
ON CONFLICT (session_id) DO UPDATE SET run_id = EXCLUDED.run_id, agent_key = EXCLUDED.agent_key, scope = EXCLUDED.scope`), [r.sessionId, r.runId, r.agentKey, r.scope]);
|
|
292
|
+
}
|
|
293
|
+
const keep = rows.map((r) => r.sessionId);
|
|
294
|
+
if (this.db.dialect === "tidb") {
|
|
295
|
+
const holes = keep.map(() => "?").join(",");
|
|
296
|
+
await this.db.query(`DELETE FROM workflow_agent_session WHERE run_id = ?${keep.length > 0 ? ` AND session_id NOT IN (${holes})` : ""}`, [runId, ...keep]);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
await this.db.query(`DELETE FROM workflow_agent_session WHERE run_id = $1${keep.length > 0 ? " AND session_id <> ALL($2::text[])" : ""}`, keep.length > 0 ? [runId, keep] : [runId]);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async findByAgentSession(sessionId) {
|
|
303
|
+
const { rows } = await this.db.query(this.q("SELECT session_id, run_id, agent_key, scope FROM workflow_agent_session WHERE session_id = ?", "SELECT session_id, run_id, agent_key, scope FROM workflow_agent_session WHERE session_id = $1"), [sessionId]);
|
|
304
|
+
const r = rows[0];
|
|
305
|
+
if (!r)
|
|
306
|
+
return null;
|
|
307
|
+
return { sessionId: String(r.session_id), runId: String(r.run_id), agentKey: String(r.agent_key), scope: String(r.scope) };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
280
310
|
export class TiDBWorkflowRunStore extends SqlWorkflowRunStore {
|
|
281
311
|
constructor(pool, onWarn) {
|
|
282
312
|
super(mysqlDriver(pool), onWarn);
|
|
@@ -307,4 +337,14 @@ export class PgWorkflowNotifyJournalStore extends SqlWorkflowNotifyJournalStore
|
|
|
307
337
|
super(pgDriver(pool));
|
|
308
338
|
}
|
|
309
339
|
}
|
|
340
|
+
export class TiDBWorkflowAgentSessionIndex extends SqlWorkflowAgentSessionIndex {
|
|
341
|
+
constructor(pool) {
|
|
342
|
+
super(mysqlDriver(pool));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
export class PgWorkflowAgentSessionIndex extends SqlWorkflowAgentSessionIndex {
|
|
346
|
+
constructor(pool) {
|
|
347
|
+
super(pgDriver(pool));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
310
350
|
//# sourceMappingURL=workflow-run-store-sql.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {} from "@sema-agent/core";
|
|
2
|
+
import { operatorAsk } from "../operator-ask.js";
|
|
2
3
|
export function routeOrchestration(s) {
|
|
3
4
|
if (s.explicitMode)
|
|
4
5
|
return { mode: s.explicitMode, reason: `explicit:${s.explicitMode}` };
|
|
@@ -40,7 +41,7 @@ export function isIsolatedExecEnv(provider, runtimeClass) {
|
|
|
40
41
|
}
|
|
41
42
|
const WRITE_HAND_TOOLS = new Set(["Edit", "Write", "NotebookEdit"]);
|
|
42
43
|
export function supPostureOverrides() {
|
|
43
|
-
const ask = (toolName) => ({
|
|
44
|
+
const ask = (toolName) => operatorAsk(`supervisor routing(capability-danger:${toolName})`, `${toolName} gated by supervisor routing (capability-danger task)`);
|
|
44
45
|
const writeGate = {
|
|
45
46
|
check: (req) => (WRITE_HAND_TOOLS.has(req.toolName) ? ask(req.toolName) : { action: "allow" }),
|
|
46
47
|
};
|
package/dist/run-local.js
CHANGED
|
@@ -190,7 +190,7 @@ export async function runLocal(argv, deps = {}) {
|
|
|
190
190
|
config.mcpServers = resolveMcpServers(effective.mcp, logger);
|
|
191
191
|
if (effective?.a2a)
|
|
192
192
|
config.a2aPeers = resolveA2aPeers(effective.a2a, logger);
|
|
193
|
-
applyCenterReadFace(config, r.effective, logger);
|
|
193
|
+
applyCenterReadFace(config, r.effective, logger, { phase: "boot" });
|
|
194
194
|
logger.info("config_loaded", { source: configProvider.kind, version: r.effective.version });
|
|
195
195
|
}
|
|
196
196
|
}
|
package/dist/runs.d.ts
CHANGED
|
@@ -324,7 +324,16 @@ parkCheckpointReader?: {
|
|
|
324
324
|
toolCallId?: unknown;
|
|
325
325
|
};
|
|
326
326
|
} | null>;
|
|
327
|
-
}
|
|
327
|
+
},
|
|
328
|
+
/** L-167:装配期(`prepareSpec` → `resolveSpec`)铸下的用户面通告 —— 那一刻本腿的通告口还没开,
|
|
329
|
+
* 所以由本腿在 `registerEngineNoticeLeg` 那一拍连同 `pending` 一起投(投递单点仍是 `route()`)。
|
|
330
|
+
* 缺席 ⇒ 一个字节不投(既有调用方逐字不变)。 */
|
|
331
|
+
pendingEngineNotices?: readonly {
|
|
332
|
+
code: string;
|
|
333
|
+
message: string;
|
|
334
|
+
detail?: Record<string, unknown>;
|
|
335
|
+
sessionId?: unknown;
|
|
336
|
+
}[]): Promise<void>;
|
|
328
337
|
/** How often a running instance refreshes its run's updated_at (liveness, independent of events).
|
|
329
338
|
* Must stay strictly below `runStaleSec` (asserted at startup AND on every hot config candidate) or the
|
|
330
339
|
* reaper would race live runs. [ref]:数值的单源是 `config-invariants.ts`(那条不变量的属主),两个消费面
|
package/dist/runs.js
CHANGED
|
@@ -88,6 +88,8 @@ export function resumeAtHttpStatus(result) {
|
|
|
88
88
|
return 400;
|
|
89
89
|
if (code === "config.invalid_checkpoint_store")
|
|
90
90
|
return 400;
|
|
91
|
+
if (code === "config.tool_mount_denied")
|
|
92
|
+
return 400;
|
|
91
93
|
if (code === "config.memory_capture_unsupported")
|
|
92
94
|
return 409;
|
|
93
95
|
if (!code.startsWith("resume_at."))
|
|
@@ -153,7 +155,7 @@ export function evictIfConflict(runner, sessionId, result) {
|
|
|
153
155
|
runner.sessions.evict?.(sessionId);
|
|
154
156
|
}
|
|
155
157
|
}
|
|
156
|
-
export async function runInBackground(runner, spec, runStore, taskId, metrics, principal, verify, cascade, instrumentDegenerate, planCacheProbe, persistThinking = false, inflight, preemptable, steerable, modelUsage, elicitation, elicitOwner, captureTurnAnchor, fleetPublisher, captureUserMessageAnchor, question, workflowCompletionInbox, subagentSteer, completionDiagLog, sendUserFile, promptManifests, approval, parkCheckpointReader) {
|
|
158
|
+
export async function runInBackground(runner, spec, runStore, taskId, metrics, principal, verify, cascade, instrumentDegenerate, planCacheProbe, persistThinking = false, inflight, preemptable, steerable, modelUsage, elicitation, elicitOwner, captureTurnAnchor, fleetPublisher, captureUserMessageAnchor, question, workflowCompletionInbox, subagentSteer, completionDiagLog, sendUserFile, promptManifests, approval, parkCheckpointReader, pendingEngineNotices) {
|
|
157
159
|
const startedAt = Date.now();
|
|
158
160
|
metrics?.addGauge("runs_active", 1);
|
|
159
161
|
fleetPublisher?.onStart();
|
|
@@ -248,6 +250,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
248
250
|
};
|
|
249
251
|
const unregisterEngineNotice = registerEngineNoticeLeg({
|
|
250
252
|
sessionId: spec.sessionId,
|
|
253
|
+
...(pendingEngineNotices !== undefined ? { pending: pendingEngineNotices } : {}),
|
|
251
254
|
durable: (row) => void append("engine_notice", row).catch(() => recordFailOpen("server.engine-notice.durable-append-failed", `leg=bg code=${row.code} task=${taskId}`)),
|
|
252
255
|
});
|
|
253
256
|
try {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { combinePolicies, COARSE_SHELL_TOOLS as CORE_COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, DEFAULT_SUBAGENT_TOOL_NAME, parseLeadingCommandName, tightenTaskSpec, } from "@sema-agent/core";
|
|
2
2
|
import { SAFETY_MERGE_SPEC } from "@sema-agent/settings-schema";
|
|
3
3
|
import { currentGovernanceAskMarks } from "./governance-ask-marks.js";
|
|
4
|
+
import { operatorAsk } from "./operator-ask.js";
|
|
4
5
|
export function isDelegationToolName(name) {
|
|
5
6
|
return name === DEFAULT_SUBAGENT_TOOL_NAME;
|
|
6
7
|
}
|
|
@@ -13,18 +14,18 @@ export const COARSE_SHELL_TOOLS_MIRROR = COARSE_SHELL_TOOLS;
|
|
|
13
14
|
function createCommandAskListPolicy(askCommands, tools = COARSE_SHELL_TOOLS) {
|
|
14
15
|
const askSet = new Set(askCommands);
|
|
15
16
|
const shellTools = new Set(tools);
|
|
16
|
-
const ask = (message) => (
|
|
17
|
+
const ask = (message, rule) => operatorAsk(rule, message);
|
|
17
18
|
return {
|
|
18
19
|
check(req) {
|
|
19
20
|
if (!shellTools.has(req.toolName))
|
|
20
21
|
return { action: "allow" };
|
|
21
22
|
const command = req.args?.command;
|
|
22
23
|
if (typeof command !== "string")
|
|
23
|
-
return ask(`tool "${req.toolName}" call has no parseable command string
|
|
24
|
+
return ask(`tool "${req.toolName}" call has no parseable command string`, "commandPolicy(unparseable command)");
|
|
24
25
|
const parsed = parseLeadingCommandName(command);
|
|
25
26
|
if ("reject" in parsed)
|
|
26
|
-
return ask(`command is not a single simple command (${parsed.reject})
|
|
27
|
-
return askSet.has(parsed.name) ? ask(`command "${parsed.name}" requires approval by policy`) : { action: "allow" };
|
|
27
|
+
return ask(`command is not a single simple command (${parsed.reject})`, "commandPolicy(argv[0] unparseable)");
|
|
28
|
+
return askSet.has(parsed.name) ? ask(`command "${parsed.name}" requires approval by policy`, `commandPolicy(${parsed.name}=ask)`) : { action: "allow" };
|
|
28
29
|
},
|
|
29
30
|
};
|
|
30
31
|
}
|
package/dist/task-mcp.d.ts
CHANGED
|
@@ -22,9 +22,25 @@
|
|
|
22
22
|
* `runtimeCapsResolver`) is a DEFERRED v2.
|
|
23
23
|
* When the gate is closed, a sent `mcpServers` is IGNORED (advertised via `capabilities.mcpInjection`).
|
|
24
24
|
*/
|
|
25
|
-
import { type CompliancePosture, type LockedKey, type McpServerSpec } from "@sema-agent/core";
|
|
25
|
+
import { type CompliancePosture, type EngineNotice, type LockedKey, type McpInjectionDropReason, type McpServerSpec } from "@sema-agent/core";
|
|
26
26
|
/** Bound on caller-supplied servers (anti-DoS; a real local `.mcp.json` has a handful). */
|
|
27
27
|
export declare const MAX_REQUEST_MCP_SERVERS = 32;
|
|
28
|
+
/**
|
|
29
|
+
* 🔴 **一类丢弃至多点名多少条**(codex 对抗复审 r1 [high],验真后采纳)—— 与挂载上限**同一个数**,
|
|
30
|
+
* 刻意不新造一个可调旋钮:点名的量级本就该与挂载的量级同阶。
|
|
31
|
+
*
|
|
32
|
+
* 病(本批引入,不是既有形):`MAX_REQUEST_MCP_SERVERS` 管的是「挂几台」,管不住「你发了几台」——
|
|
33
|
+
* HTTP 体上限 8 MiB 装得下十万个名字,而 `gate_closed`(整只列表被忽略)与 `over_cap`(上限之后的尾项)
|
|
34
|
+
* 两臂原本逐条点名,于是一条已认证请求就能造出十万条事实、十万条通告,逐条排进本腿的**串行账本写链**
|
|
35
|
+
* ——同一条链上后续的正常事件全部排在它们后面。修前的名字数组把超上限折成**一条**聚合串,所以无界的
|
|
36
|
+
* 是本批新写的那两个循环。
|
|
37
|
+
*
|
|
38
|
+
* 为什么不改成「数组太长就 400」(codex 的另一条建议,**未采纳**):今天 33 台的请求是**照常受理、挂前
|
|
39
|
+
* 32 台**的,加一条新的拒绝面为了一个资源边界去改受理语义,爆炸半径比病本身大(而且 resume 腿重放存量
|
|
40
|
+
* body 会当场砖死一批已 park 的任务——本仓 `opts.leg` 三处先例逐字写着「砖死 resume 比降级糟」)。
|
|
41
|
+
* 有界点名两头都占:该说的仍然说得出,量级钉死在与挂载同阶。
|
|
42
|
+
*/
|
|
43
|
+
export declare const MAX_REQUEST_MCP_DROP_NAMES = 32;
|
|
28
44
|
/**
|
|
29
45
|
* Does THIS deployment honor caller-supplied per-request MCP servers? TRUE for a SINGLE-USER deployment
|
|
30
46
|
* (`requirePrincipal !== true`) — the requester is the super-admin of their own worker, so their `.mcp.json` is
|
|
@@ -37,19 +53,42 @@ export declare function mcpInjectionHonored(config: {
|
|
|
37
53
|
lockedConfigKeys?: readonly LockedKey[];
|
|
38
54
|
compliancePosture?: CompliancePosture;
|
|
39
55
|
}): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* L-167(core 7.9.1)—— 一条**没有被挂上**的请求腿 MCP 条目的**事实**。宿主报事实,core 铸词:
|
|
58
|
+
* `reason` 的词表属主是 core(`McpInjectionDropReason` = malformed_entry / name_reserved_by_deployment /
|
|
59
|
+
* gate_closed / over_cap),本仓一个词都不枚举,通告的码/受众/句子一个字都不拼。
|
|
60
|
+
*
|
|
61
|
+
* `sessionId` **不在**这个形上:它是通告的**路由半场**,在铸点(`mintInjectionDropNotices`)才补 ——
|
|
62
|
+
* 一条丢弃事实在没有会话语境的调用方(桩、单测、未来的非 HTTP 腿)那里同样成立,而通告不成立。
|
|
63
|
+
*/
|
|
64
|
+
export interface McpInjectionDrop {
|
|
65
|
+
readonly server: string;
|
|
66
|
+
readonly reason: McpInjectionDropReason;
|
|
67
|
+
/** `malformed_entry` 专用:**哪个键**判不出形。结构性给出(读器在拒的那一行就知道),不从文案里 parse。 */
|
|
68
|
+
readonly field?: string;
|
|
69
|
+
}
|
|
70
|
+
/** L-167:本条腿的用户面通告席。`sessionId` = 路由半场(`auth.sessionId`,与 `spec.sessionId` 同源);
|
|
71
|
+
* `emit` = 收下 core 铸好的 `EngineNotice`。**本模块不知道**它最后落在 SSE、账本还是日志上 —— 那是
|
|
72
|
+
* 投递腿的事,本模块只负责「事实 → core 工厂 → 一条通告」。 */
|
|
73
|
+
export interface McpInjectionDropSeat {
|
|
74
|
+
readonly sessionId: string | undefined;
|
|
75
|
+
readonly emit: (notice: EngineNotice) => void;
|
|
76
|
+
}
|
|
40
77
|
/**
|
|
41
78
|
* Shape-check the raw `body.mcpServers`. `null` → a non-array (caller error; the route may 400). Otherwise the
|
|
42
79
|
* valid subset + the names of dropped (malformed/over-cap) entries + [ref]'s `badContentOrigin`
|
|
43
80
|
* (**降级**掉的内容出处声明,与 `dropped` 是两回事:那条 server **留着**,丢的只是它的声明)。
|
|
44
81
|
*/
|
|
45
|
-
export declare function validateRequestMcp(raw: unknown):
|
|
82
|
+
export declare function validateRequestMcp(raw: unknown): ValidatedRequestMcp | null;
|
|
83
|
+
type ValidatedRequestMcp = {
|
|
46
84
|
ok: McpServerSpec[];
|
|
47
|
-
dropped:
|
|
85
|
+
dropped: McpInjectionDrop[];
|
|
86
|
+
droppedTotal: number;
|
|
48
87
|
badContentOrigin: Array<{
|
|
49
88
|
server: string;
|
|
50
89
|
value: string;
|
|
51
90
|
}>;
|
|
52
|
-
}
|
|
91
|
+
};
|
|
53
92
|
/**
|
|
54
93
|
* [ref] 件C §4.3 —— **同步**拒面:部署锁了 `mcp` 而请求仍带 `mcpServers` ⇒ 400 `config.locked_key`。
|
|
55
94
|
*
|
|
@@ -128,7 +167,7 @@ export declare function mergeRequestMcp(baseline: McpServerSpec[] | undefined, g
|
|
|
128
167
|
/**
|
|
129
168
|
* The single entry point the spec-builder calls: compute the effective `TaskSpec.mcp` = deployment baseline + the
|
|
130
169
|
* caller's gated per-request servers. Off the honored lane (or with no caller servers) returns the baseline
|
|
131
|
-
* unchanged. Logs an `
|
|
170
|
+
* unchanged. Logs an `mcp_injection_dropped` (reason `gate_closed`) when a caller SENT servers a closed gate dropped (so the shell never
|
|
132
171
|
* silently believes its `.mcp.json` took effect — it also reads `capabilities.mcpInjection`).
|
|
133
172
|
*/
|
|
134
173
|
export declare function resolveRequestMcp(baseline: McpServerSpec[] | undefined, bodyMcp: unknown, config: {
|
|
@@ -144,5 +183,10 @@ export declare function resolveRequestMcp(baseline: McpServerSpec[] | undefined,
|
|
|
144
183
|
}, logger?: {
|
|
145
184
|
info?(m: string, meta?: unknown): void;
|
|
146
185
|
warn(m: string, meta?: unknown): void;
|
|
147
|
-
}
|
|
186
|
+
},
|
|
187
|
+
/** L-167:本条腿的**用户面**投递口。`sessionId` = 通告的路由半场(缺席 ⇒ 一条都不铸,见
|
|
188
|
+
* {@link mintInjectionDropNotices});`emit` 收下 core 铸好的通告,由调用腿决定投到哪(见 `boot/resolve-spec.ts`)。
|
|
189
|
+
* 整段缺席(桩、单测、非 HTTP 腿)⇒ 只走日志终点,行为逐字与本件之前相同。 */
|
|
190
|
+
notice?: McpInjectionDropSeat): McpServerSpec[] | undefined;
|
|
191
|
+
export {};
|
|
148
192
|
//# sourceMappingURL=task-mcp.d.ts.map
|