@gt-fe/eap-runtime 0.0.21 → 0.0.23

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.
Files changed (60) hide show
  1. package/README.md +1 -1
  2. package/dist/api/handlers/invoke.handler.js +6 -3
  3. package/dist/api/handlers/stream.handler.js +6 -2
  4. package/dist/checkpoint/file-checkpoint-saver.d.ts +4 -0
  5. package/dist/checkpoint/file-checkpoint-saver.js +51 -22
  6. package/dist/checkpoint/index.d.ts +1 -1
  7. package/dist/checkpoint/index.js +1 -1
  8. package/dist/graph/adapters/langgraph-checkpointer.d.ts +11 -0
  9. package/dist/graph/adapters/langgraph-checkpointer.js +48 -9
  10. package/dist/graph/adapters/langgraph.adapter.d.ts +8 -0
  11. package/dist/graph/adapters/langgraph.adapter.js +58 -16
  12. package/dist/graph/adapters/skill-isolate.d.ts +1 -5
  13. package/dist/graph/adapters/skill-isolate.js +1 -12
  14. package/dist/graph/adapters/subgraph-compiler.js +12 -15
  15. package/dist/graph/contracts/executable-graph.d.ts +6 -0
  16. package/dist/graph/hitl/hitl-handler.d.ts +13 -4
  17. package/dist/graph/hitl/hitl-handler.js +78 -7
  18. package/dist/graph/nodes/tool-node.js +11 -6
  19. package/dist/providers/index.d.ts +1 -1
  20. package/dist/providers/index.js +1 -1
  21. package/dist/providers/local-llm-provider.d.ts +6 -0
  22. package/dist/providers/local-llm-provider.js +29 -5
  23. package/dist/providers/routing-llm-provider.d.ts +2 -2
  24. package/dist/providers/routing-llm-provider.js +5 -15
  25. package/dist/registry/deployment-snapshot.js +18 -4
  26. package/dist/registry/graph-loader.d.ts +2 -0
  27. package/dist/registry/graph-loader.js +99 -7
  28. package/dist/registry/preload-flags.d.ts +1 -1
  29. package/dist/registry/preload-flags.js +1 -1
  30. package/dist/runtime/agent-runtime.js +27 -10
  31. package/dist/runtime/dev-runtime.d.ts +2 -1
  32. package/dist/runtime/dev-runtime.js +9 -3
  33. package/dist/session/apply-session-compression.d.ts +4 -0
  34. package/dist/session/apply-session-compression.js +22 -3
  35. package/dist/session/index.d.ts +1 -1
  36. package/dist/session/index.js +1 -1
  37. package/dist/session/persist-turn-messages.d.ts +6 -1
  38. package/dist/session/persist-turn-messages.js +23 -5
  39. package/dist/token/record-task-token-usage.d.ts +23 -0
  40. package/dist/token/record-task-token-usage.js +20 -0
  41. package/dist/tool/http-tool-executor.js +7 -3
  42. package/dist/tool/index.d.ts +2 -1
  43. package/dist/tool/index.js +2 -1
  44. package/dist/tool/loaded-tool-definition.d.ts +1 -1
  45. package/dist/tool/local-mcp-client.d.ts +1 -1
  46. package/dist/tool/local-mcp-client.js +2 -39
  47. package/dist/tool/local-tool-executor.js +32 -5
  48. package/dist/tool/mcp-list.d.ts +21 -0
  49. package/dist/tool/mcp-list.js +49 -3
  50. package/dist/tool/mcp-tool-executor.d.ts +1 -1
  51. package/dist/tool/mcp-tool-executor.js +58 -22
  52. package/dist/tool/model-visible-tool-content.d.ts +16 -0
  53. package/dist/tool/model-visible-tool-content.js +266 -0
  54. package/dist/tool/timeout.d.ts +5 -12
  55. package/dist/tool/timeout.js +12 -34
  56. package/dist/tool/tool-confirmation.js +8 -5
  57. package/dist/tool/tool-executor.interface.d.ts +7 -1
  58. package/dist/tool/tool-orchestrator.js +6 -1
  59. package/dist/tool/tool-result-injector.js +2 -1
  60. package/package.json +2 -2
@@ -8,7 +8,6 @@
8
8
  * @see design.md §1.4 子图嵌套: Skill作为子图嵌入主Graph
9
9
  * @see requirements.md Requirement 1 AC2 — 子图嵌套(Skill作为子图)
10
10
  */
11
- import { AppError, ErrorCodes } from '@gt-fe/eap-contracts';
12
11
  import { assertSkillPermission } from '../../governance/permission-guard.js';
13
12
  import { LangGraphAdapter } from './langgraph.adapter.js';
14
13
  import { assertSkillGraphHasNoSubgraphNodes } from './graph-nesting.js';
@@ -18,7 +17,7 @@ import { filterLoadedToolsForSkill, } from '../../tool/system-prompt-builder.js'
18
17
  import { unwrapHitlPaused } from '../hitl/hitl-handler.js';
19
18
  import { pickContextFields } from '../../token/context-window.js';
20
19
  import { assertSkillIoIfEnabled, coerceSkillInputFromMessages, } from '../../registry/skill-io-guard.js';
21
- import { isolatedSkillMessages, isolatedSkillOutput, SKILL_ISOLATE_HITL_MESSAGE, } from './skill-isolate.js';
20
+ import { isolatedSkillMessages, isolatedSkillOutput, } from './skill-isolate.js';
22
21
  /**
23
22
  * SubGraphCompiler — 子图编译器
24
23
  *
@@ -150,15 +149,15 @@ export class SubGraphCompiler {
150
149
  : parentMessages;
151
150
  const subGraphState = {
152
151
  messages: subMessagesIn,
153
- // 默认同父 session thread;隔离时不写父 Checkpoint,避免短历史覆盖父对话
152
+ // 同父 session;checkpointNs=父节点 id → 子图槽 thread_id#父节点id(HITL 同槽,含 isolate)。
154
153
  replaceCheckpointMessages: !isolateContext,
155
154
  userId,
156
155
  delegationToken,
157
156
  agentCode,
158
157
  taskId: state['taskId'],
159
158
  correlationId: state['correlationId'],
160
- sessionId: isolateContext ? undefined : state['sessionId'],
161
- // ToolNode 用此标志在 interrupt 前拒绝 HITL
159
+ sessionId: state['sessionId'],
160
+ checkpointNs: options?.parentNodeId,
162
161
  skillIsolateContext: isolateContext || undefined,
163
162
  executionContext: state['executionContext'],
164
163
  loadedTools: scopedTools,
@@ -175,6 +174,8 @@ export class SubGraphCompiler {
175
174
  lastSpanId: skillSpanId ?? state['lastSpanId'],
176
175
  hitlChoice: state['hitlChoice'],
177
176
  hitlRespondedBy: state['hitlRespondedBy'],
177
+ toolSpanId: typeof state['toolSpanId'] === 'string' ? state['toolSpanId'] : undefined,
178
+ hitlToolCallId: typeof state['hitlToolCallId'] === 'string' ? state['hitlToolCallId'] : undefined,
178
179
  };
179
180
  const result = await adapter.runIsolatedLlmStreamFlags(() => adapter.invoke(subGraph, subGraphState));
180
181
  // 进程内调试记录;生产记录是上面的 skill Span
@@ -229,10 +230,12 @@ export class SubGraphCompiler {
229
230
  const nestedLastSpanId = (typeof result.finalState?.lastSpanId === 'string' && result.finalState.lastSpanId) ||
230
231
  skillSpanId ||
231
232
  undefined;
233
+ const parentNodeId = options?.parentNodeId
234
+ || (dsl.name ? `subgraph:${dsl.name}` : 'subgraph');
232
235
  return {
233
236
  messages: outputMessages,
234
- currentNode: options?.parentNodeId
235
- || (dsl.name ? `subgraph:${dsl.name}` : 'subgraph'),
237
+ currentNode: parentNodeId,
238
+ historyPath: [parentNodeId],
236
239
  ...(nestedLastSpanId ? { lastSpanId: nestedLastSpanId } : {}),
237
240
  ...(Array.isArray(perNode) && perNode.length > 0
238
241
  ? {
@@ -249,16 +252,10 @@ export class SubGraphCompiler {
249
252
  }
250
253
  catch (err) {
251
254
  const hitlPaused = unwrapHitlPaused(err);
252
- // 未隔离:HITL 暂停原样上抛。隔离:转 GRAPH_EXECUTION_ERROR,走 skill.failed,不当 waiting_hitl。
253
- if (hitlPaused && !isolateContext) {
255
+ if (hitlPaused) {
254
256
  throw err;
255
257
  }
256
- const failErr = hitlPaused && isolateContext
257
- ? new AppError(ErrorCodes.GRAPH_EXECUTION_ERROR, SKILL_ISOLATE_HITL_MESSAGE, {
258
- exposeMessage: true,
259
- cause: err,
260
- })
261
- : err;
258
+ const failErr = err;
262
259
  const failMessage = failErr instanceof Error ? failErr.message : String(failErr);
263
260
  await auditCtx?.finish({
264
261
  status: 'failed',
@@ -243,6 +243,12 @@ export interface GraphState {
243
243
  * 仅 stream/invoke 入口使用,不入持久化语义。
244
244
  */
245
245
  replaceCheckpointMessages?: boolean;
246
+ /**
247
+ * 子图 invoke 写入 LangGraph `checkpoint_ns`。
248
+ * 父图为空;子图为父节点 id。存储槽与父 thread 分开,不覆盖父 latest。
249
+ * HITL interrupt 用同一槽;resume 返回给父图入口的 GraphState 不含本字段,也不含内层 historyPath / currentNode。
250
+ */
251
+ checkpointNs?: string;
246
252
  [key: string]: unknown;
247
253
  }
248
254
  /** Graph 内部消息格式 */
@@ -13,7 +13,7 @@
13
13
  import type { ICheckpointSaver } from '../../checkpoint/contracts/checkpoint-saver.contract.js';
14
14
  import type { ITraceCollector } from '../../contracts/trace-collector.js';
15
15
  import type { ISSEEmitter } from '../../sse/contracts/sse-emitter.contract.js';
16
- import type { GraphState } from '../contracts/executable-graph.js';
16
+ import type { GraphState, CheckpointData } from '../contracts/executable-graph.js';
17
17
  /** HITL 默认等待时长(毫秒)= 300 秒。SSE `hitl.waited.timeout` 对外用秒。 */
18
18
  export declare const DEFAULT_HITL_TIMEOUT_MS = 300000;
19
19
  /** 超时收口对外文案(Task / Trace / AppError);枚举走 `hitl_timeout`。 */
@@ -106,6 +106,8 @@ export declare class HitlPausedError extends Error {
106
106
  export declare function unwrapHitlPaused(error: unknown): HitlPausedError | null;
107
107
  /** Checkpoint state 中持久化的 HITL 等待元数据(进程重启 / 无内存 Map 时冷恢复) */
108
108
  export declare const HITL_WAITING_STATE_KEY = "__hitlWaiting";
109
+ /** 父槽上的指针:子图 HITL 的 waiting 在 `sessionId#父节点id`,冷恢复只带 sessionId 时跟随此键。 */
110
+ export declare const HITL_WAITING_SLOT_KEY = "__hitlWaitingSlot";
109
111
  export interface PersistedHitlWaiting {
110
112
  taskId: string;
111
113
  threadId: string;
@@ -141,6 +143,12 @@ export declare function toHitlTimeoutMs(timeoutMs?: number): number;
141
143
  export declare function toHitlTimeoutSeconds(timeoutMs?: number): number;
142
144
  export declare function isHitlDeadlinePassed(persisted: Pick<PersistedHitlWaiting, 'deadlineAt'> | undefined, now?: number): boolean;
143
145
  export declare function readPersistedHitlWaiting(state: unknown): PersistedHitlWaiting | undefined;
146
+ export declare function readHitlWaitingSlot(state: unknown): string | undefined;
147
+ /** 父槽有 waiting 用父槽;否则跟随 `__hitlWaitingSlot` 到子图槽。 */
148
+ export declare function loadHitlCheckpoint(saver: ICheckpointSaver, threadId: string): Promise<{
149
+ threadId: string;
150
+ checkpoint: CheckpointData | null;
151
+ }>;
144
152
  /** 门面 stream / HTTP StreamOrchestrator / streamResumeHitl 共用 */
145
153
  export declare function buildHitlWaitedPayload(paused: HitlPausedError): {
146
154
  question: string;
@@ -177,9 +185,10 @@ export declare class HitlHandler {
177
185
  /**
178
186
  * 中断 Graph 执行,等待人工确认
179
187
  *
180
- * 写入 `__hitlWaiting`(含 deadlineAt)到同一 thread 的 Checkpoint。
181
- * 不保留 LangGraph `engineSnapshot`:子图与父图共用 session thread,
182
- * resume 时用 GraphState 从父图入口重入(skillSpanId / hitlChoice 防重复 span)。
188
+ * 写入 `__hitlWaiting`(含 deadlineAt)到当前执行图的 Checkpoint 槽。
189
+ * 父图槽=sessionId;子图槽=sessionId#父节点id(与 LangGraph put 同一把钥匙)。
190
+ * 不保留 LangGraph `engineSnapshot`:resume 从父图入口重入
191
+ *(skillSpanId / hitlChoice 防重复 span;子图 resume 去掉 checkpointNs / 内层 historyPath / currentNode)。
183
192
  */
184
193
  interrupt(taskId: string, threadId: string, state: GraphState, config: HitlInterruptConfig, extras?: HitlInterruptExtras): Promise<void>;
185
194
  private interruptCore;
@@ -118,6 +118,8 @@ export function unwrapHitlPaused(error) {
118
118
  }
119
119
  /** Checkpoint state 中持久化的 HITL 等待元数据(进程重启 / 无内存 Map 时冷恢复) */
120
120
  export const HITL_WAITING_STATE_KEY = '__hitlWaiting';
121
+ /** 父槽上的指针:子图 HITL 的 waiting 在 `sessionId#父节点id`,冷恢复只带 sessionId 时跟随此键。 */
122
+ export const HITL_WAITING_SLOT_KEY = '__hitlWaitingSlot';
121
123
  export function toHitlTimeoutMs(timeoutMs) {
122
124
  if (typeof timeoutMs !== 'number' || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
123
125
  return DEFAULT_HITL_TIMEOUT_MS;
@@ -142,6 +144,29 @@ export function readPersistedHitlWaiting(state) {
142
144
  return undefined;
143
145
  return raw;
144
146
  }
147
+ export function readHitlWaitingSlot(state) {
148
+ if (!state || typeof state !== 'object')
149
+ return undefined;
150
+ const raw = state[HITL_WAITING_SLOT_KEY];
151
+ if (typeof raw === 'string' && raw.trim())
152
+ return raw.trim();
153
+ return undefined;
154
+ }
155
+ /** 父槽有 waiting 用父槽;否则跟随 `__hitlWaitingSlot` 到子图槽。 */
156
+ export async function loadHitlCheckpoint(saver, threadId) {
157
+ const first = await saver.load(threadId);
158
+ if (readPersistedHitlWaiting(first?.state)) {
159
+ return { threadId, checkpoint: first };
160
+ }
161
+ const slot = readHitlWaitingSlot(first?.state);
162
+ if (slot && slot !== threadId) {
163
+ const nested = await saver.load(slot);
164
+ if (readPersistedHitlWaiting(nested?.state)) {
165
+ return { threadId: slot, checkpoint: nested };
166
+ }
167
+ }
168
+ return { threadId, checkpoint: first };
169
+ }
145
170
  /** 门面 stream / HTTP StreamOrchestrator / streamResumeHitl 共用 */
146
171
  export function buildHitlWaitedPayload(paused) {
147
172
  return {
@@ -243,9 +268,10 @@ export class HitlHandler {
243
268
  /**
244
269
  * 中断 Graph 执行,等待人工确认
245
270
  *
246
- * 写入 `__hitlWaiting`(含 deadlineAt)到同一 thread 的 Checkpoint。
247
- * 不保留 LangGraph `engineSnapshot`:子图与父图共用 session thread,
248
- * resume 时用 GraphState 从父图入口重入(skillSpanId / hitlChoice 防重复 span)。
271
+ * 写入 `__hitlWaiting`(含 deadlineAt)到当前执行图的 Checkpoint 槽。
272
+ * 父图槽=sessionId;子图槽=sessionId#父节点id(与 LangGraph put 同一把钥匙)。
273
+ * 不保留 LangGraph `engineSnapshot`:resume 从父图入口重入
274
+ *(skillSpanId / hitlChoice 防重复 span;子图 resume 去掉 checkpointNs / 内层 historyPath / currentNode)。
249
275
  */
250
276
  async interrupt(taskId, threadId, state, config, extras = {}) {
251
277
  const work = () => this.interruptCore(taskId, threadId, state, config, extras);
@@ -300,6 +326,28 @@ export class HitlHandler {
300
326
  };
301
327
  const persistedCheckpoint = stripCheckpointCollectors(checkpointData);
302
328
  await this.checkpointSaver.save(threadId, persistedCheckpoint);
329
+ const parentSessionId = typeof state.sessionId === 'string' && state.sessionId.trim()
330
+ ? state.sessionId.trim()
331
+ : '';
332
+ if (parentSessionId && threadId !== parentSessionId) {
333
+ const parent = await this.checkpointSaver.load(parentSessionId);
334
+ const pointerState = {
335
+ ...(parent?.state ?? { messages: [], userId: state.userId ?? '', sessionId: parentSessionId }),
336
+ [HITL_WAITING_SLOT_KEY]: threadId,
337
+ };
338
+ await this.checkpointSaver.save(parentSessionId, stripCheckpointCollectors({
339
+ checkpointId: parent?.checkpointId ?? `hitl-slot-${taskId}`,
340
+ threadId: parentSessionId,
341
+ currentNodeId: parent?.currentNodeId ?? '__hitl_waiting__',
342
+ historyPath: parent?.historyPath ?? [],
343
+ state: pointerState,
344
+ createdBy: parent?.createdBy ?? 'system',
345
+ createdAt: parent?.createdAt ?? now,
346
+ updatedBy: 'system',
347
+ updatedAt: now,
348
+ isDeleted: false,
349
+ }));
350
+ }
303
351
  const liveCollector = getExecutionTrace()?.collector;
304
352
  if (liveCollector) {
305
353
  this.waitingCollectors.set(taskId, liveCollector);
@@ -352,13 +400,14 @@ export class HitlHandler {
352
400
  async resume(taskId, choice, userId, threadIdHint) {
353
401
  return this.withTaskLock(taskId, async () => {
354
402
  let waiting = this.waitingTasks.get(taskId);
355
- const threadId = waiting?.threadId ?? threadIdHint;
356
- if (!threadId) {
403
+ const hinted = waiting?.threadId ?? threadIdHint;
404
+ if (!hinted) {
357
405
  throw new HitlNotWaitingError(taskId);
358
406
  }
359
- const checkpoint = await this.checkpointSaver.load(threadId);
407
+ const resolved = await loadHitlCheckpoint(this.checkpointSaver, hinted);
408
+ const checkpoint = resolved.checkpoint;
360
409
  if (!checkpoint) {
361
- throw new Error(`Checkpoint not found for thread: ${threadId}`);
410
+ throw new Error(`Checkpoint not found for thread: ${resolved.threadId}`);
362
411
  }
363
412
  const persisted = readPersistedHitlWaiting(checkpoint.state);
364
413
  if (persisted?.timedOut) {
@@ -380,6 +429,13 @@ export class HitlHandler {
380
429
  const baseState = prepareHitlCheckpointState(checkpoint.state);
381
430
  const cleaned = { ...baseState };
382
431
  delete cleaned[HITL_WAITING_STATE_KEY];
432
+ const subgraphNs = typeof cleaned['checkpointNs'] === 'string' ? String(cleaned['checkpointNs']).trim() : '';
433
+ delete cleaned['checkpointNs'];
434
+ if (subgraphNs) {
435
+ // 父图入口重入:内层路径不能带进父图,否则 historyPath 混入子图节点。
436
+ delete cleaned['historyPath'];
437
+ delete cleaned['currentNode'];
438
+ }
383
439
  const resumedState = {
384
440
  ...cleaned,
385
441
  hitlChoice: choice,
@@ -395,6 +451,21 @@ export class HitlHandler {
395
451
  updatedBy: userId,
396
452
  updatedAt: now,
397
453
  }));
454
+ const parentSessionId = typeof resumedState.sessionId === 'string' && resumedState.sessionId.trim()
455
+ ? resumedState.sessionId.trim()
456
+ : '';
457
+ if (parentSessionId && waiting.threadId !== parentSessionId) {
458
+ const parent = await this.checkpointSaver.load(parentSessionId);
459
+ if (readHitlWaitingSlot(parent?.state) === waiting.threadId && parent) {
460
+ const parentState = { ...parent.state };
461
+ delete parentState[HITL_WAITING_SLOT_KEY];
462
+ await this.checkpointSaver.save(parentSessionId, stripCheckpointCollectors({
463
+ ...parent,
464
+ state: parentState,
465
+ updatedAt: now,
466
+ }));
467
+ }
468
+ }
398
469
  const hitlSpanId = waiting.hitlSpanId ?? persisted?.hitlSpanId;
399
470
  const resumeTraceId = resolveTraceId(resumedState) ?? resolveTraceId(checkpoint.state);
400
471
  const resumeCtx = liveCollector && resumeTraceId
@@ -16,12 +16,12 @@ import { inferHarnessSkillDir } from '../../tool/skill-dir-infer.js';
16
16
  import { isHitlDecision, unwrapHitlPaused } from '../hitl/hitl-handler.js';
17
17
  import { ToolConfirmationInterceptor } from '../../tool/tool-confirmation.js';
18
18
  import { resolveCheckpointThreadId } from '../../session/persist-turn-messages.js';
19
- import { toolTraceIdentity } from '../../tool/mcp-list.js';
19
+ import { mcpToolsetCode, toolTraceIdentity } from '../../tool/mcp-list.js';
20
20
  import { resolveExecutionTrace, warnTraceSpanFailure, } from '../execution-trace-context.js';
21
21
  import { emitRuntimeLog } from '../../common/logger.js';
22
22
  import { decideBeforeTool, } from '../../execution/interceptors.js';
23
23
  import { isRetryableToolFailure, resolveToolRetryConfig, sleepToolRetry, toolRetryDelayMs, } from '../../tool/tool-retry.js';
24
- import { assertIsolatedSkillAllowsHitl, isIsolatedSkillState } from '../adapters/skill-isolate.js';
24
+ import { isIsolatedSkillState } from '../adapters/skill-isolate.js';
25
25
  function skillDirsFromState(state) {
26
26
  const defs = state['skillDefinitions'];
27
27
  return Object.values(defs ?? {})
@@ -92,6 +92,7 @@ export class ToolNode {
92
92
  // 4. 转换为 ToolCallRequest[](附带 manifest 路由字段)
93
93
  const requests = lastAssistantMessage.toolCalls.map((tc) => {
94
94
  const loaded = loadedTools?.[tc.name];
95
+ const toolsetCode = mcpToolsetCode(loaded);
95
96
  const correlationId = String(state['correlationId'] ?? tc.id);
96
97
  const taskId = String(state['taskId'] ?? state['correlationId'] ?? tc.id);
97
98
  const command = typeof tc.arguments?.command === 'string' ? tc.arguments.command : undefined;
@@ -114,6 +115,7 @@ export class ToolNode {
114
115
  // dev 上下文 DevMcpExecutor 直连所需;server 上下文会忽略
115
116
  execution: loaded?.manifest.execution,
116
117
  ...(loaded?.manifest.version ? { toolVersion: loaded.manifest.version } : {}),
118
+ ...(toolsetCode ? { toolsetCode } : {}),
117
119
  correlationId,
118
120
  taskId,
119
121
  ...(jail.workdir ? { jailWorkdir: jail.workdir } : {}),
@@ -231,10 +233,6 @@ export class ToolNode {
231
233
  else {
232
234
  const askHitl = gate.action === 'ask' || (needsHitl && confirmationTools.has(req.toolName));
233
235
  if (askHitl && this.hitlHandler) {
234
- // 隔离子图未按独立 HITL 线程设计;interrupt 前拒绝,避免写错父 Checkpoint。
235
- if (!resumingHitl) {
236
- assertIsolatedSkillAllowsHitl(state);
237
- }
238
236
  const confirmSet = new Set(confirmationTools);
239
237
  if (gate.action === 'ask')
240
238
  confirmSet.add(req.toolName);
@@ -510,10 +508,17 @@ export class ToolNode {
510
508
  const delegationToken = String(executionContext?.delegationToken ??
511
509
  state['delegationToken'] ??
512
510
  this.defaultAuthContext.delegationToken);
511
+ const fromState = typeof state['agentVersion'] === 'string' ? state['agentVersion'].trim() : '';
512
+ const agentDir = typeof state['agentDir'] === 'string' ? state['agentDir'].trim() : '';
513
+ const fromDir = agentDir
514
+ ? agentDir.split(/[/\\]/).filter(Boolean).pop()
515
+ : undefined;
516
+ const agentVersion = fromState || fromDir;
513
517
  return {
514
518
  userId,
515
519
  delegationToken,
516
520
  agentCode,
521
+ ...(agentVersion ? { agentVersion } : {}),
517
522
  };
518
523
  }
519
524
  shouldForceServer(state, toolName) {
@@ -2,6 +2,6 @@ export { EchoLLMProvider, createDefaultLLMProvider } from './echo-llm-provider.j
2
2
  export { HttpLLMProvider, DEFAULT_LLM_HTTP_TIMEOUT_MS, type HttpLLMProviderConfig } from './http-llm-provider.js';
3
3
  export { RoutingLLMProvider, type RoutingLLMProviderConfig, assertFoundationModelsInvokeAllowsStream, isFoundationModelsInvokeEnabled, MODELS_INVOKE_STREAM_MESSAGE, } from './routing-llm-provider.js';
4
4
  export { resolveLlmModelName, canOverrideGraphModel } from './resolve-llm-model.js';
5
- export { createLocalLlmProvider, requireLocalLlmConnections, resolveLocalLlmConnection, parseLocalLlmConnectionRefs, hydrateLocalLlmConnections, LLM_CONNECTIONS_ENV, } from './local-llm-provider.js';
5
+ export { createLocalLlmProvider, createOptionalLocalLlmProvider, requireLocalLlmConnections, resolveLocalLlmConnection, parseLocalLlmConnectionRefs, hydrateLocalLlmConnections, hasLocalLlmConnection, LLM_CONNECTIONS_ENV, } from './local-llm-provider.js';
6
6
  export type { LocalLlmConnection, LocalLlmConnectionRef, LocalLlmConnectionRefs, LocalLlmConnections, } from './local-llm-provider.js';
7
7
  export type { LLMToolDefinition } from '../graph/nodes/llm-node.js';
@@ -2,4 +2,4 @@ export { EchoLLMProvider, createDefaultLLMProvider } from './echo-llm-provider.j
2
2
  export { HttpLLMProvider, DEFAULT_LLM_HTTP_TIMEOUT_MS } from './http-llm-provider.js';
3
3
  export { RoutingLLMProvider, assertFoundationModelsInvokeAllowsStream, isFoundationModelsInvokeEnabled, MODELS_INVOKE_STREAM_MESSAGE, } from './routing-llm-provider.js';
4
4
  export { resolveLlmModelName, canOverrideGraphModel } from './resolve-llm-model.js';
5
- export { createLocalLlmProvider, requireLocalLlmConnections, resolveLocalLlmConnection, parseLocalLlmConnectionRefs, hydrateLocalLlmConnections, LLM_CONNECTIONS_ENV, } from './local-llm-provider.js';
5
+ export { createLocalLlmProvider, createOptionalLocalLlmProvider, requireLocalLlmConnections, resolveLocalLlmConnection, parseLocalLlmConnectionRefs, hydrateLocalLlmConnections, hasLocalLlmConnection, LLM_CONNECTIONS_ENV, } from './local-llm-provider.js';
@@ -29,6 +29,12 @@ export interface LocalLlmConnections {
29
29
  export declare const LLM_CONNECTIONS_ENV = "EAP_LLM_CONNECTIONS";
30
30
  export declare function parseLocalLlmConnectionRefs(env?: NodeJS.ProcessEnv): LocalLlmConnectionRefs;
31
31
  export declare function hydrateLocalLlmConnections(refs: LocalLlmConnectionRefs, env?: NodeJS.ProcessEnv): LocalLlmConnections;
32
+ export declare function hasLocalLlmConnection(connections: LocalLlmConnections): boolean;
32
33
  export declare function requireLocalLlmConnections(env?: NodeJS.ProcessEnv): LocalLlmConnections;
34
+ /**
35
+ * Routing 的可选 fallback:有可用本地连接才返回 Provider。
36
+ * 连接表 JSON 损坏仍抛错;解不出密钥则视为没有本地网关。
37
+ */
38
+ export declare function createOptionalLocalLlmProvider(turnLog?: ILlmTurnLog, env?: NodeJS.ProcessEnv): ILLMProvider | undefined;
33
39
  export declare function resolveLocalLlmConnection(connections: LocalLlmConnections, model?: string): LocalLlmConnection;
34
40
  export declare function createLocalLlmProvider(connections?: LocalLlmConnections, turnLog?: ILlmTurnLog): ILLMProvider;
@@ -63,10 +63,13 @@ export function hydrateLocalLlmConnections(refs, env = process.env) {
63
63
  }
64
64
  return { default: hydrate(refs.default), byModel };
65
65
  }
66
+ export function hasLocalLlmConnection(connections) {
67
+ return Boolean(connections.default?.apiKey) || Object.keys(connections.byModel ?? {}).length > 0;
68
+ }
66
69
  export function requireLocalLlmConnections(env = process.env) {
67
70
  const refs = parseLocalLlmConnectionRefs(env);
68
71
  const connections = hydrateLocalLlmConnections(refs, env);
69
- if (!connections.default && Object.keys(connections.byModel ?? {}).length === 0) {
72
+ if (!hasLocalLlmConnection(connections)) {
70
73
  const wanted = [
71
74
  refs.default?.apiKeyEnv,
72
75
  ...Object.values(refs.byModel ?? {}).map((ref) => ref.apiKeyEnv),
@@ -75,13 +78,34 @@ export function requireLocalLlmConnections(env = process.env) {
75
78
  }
76
79
  return connections;
77
80
  }
81
+ /**
82
+ * Routing 的可选 fallback:有可用本地连接才返回 Provider。
83
+ * 连接表 JSON 损坏仍抛错;解不出密钥则视为没有本地网关。
84
+ */
85
+ export function createOptionalLocalLlmProvider(turnLog, env = process.env) {
86
+ const refs = parseLocalLlmConnectionRefs(env);
87
+ const connections = hydrateLocalLlmConnections(refs, env);
88
+ if (!hasLocalLlmConnection(connections))
89
+ return undefined;
90
+ return createLocalLlmProvider(connections, turnLog);
91
+ }
92
+ function namedLocalModel(model) {
93
+ const trimmed = model?.trim();
94
+ if (!trimmed || trimmed.toLowerCase() === 'default')
95
+ return undefined;
96
+ return trimmed;
97
+ }
78
98
  export function resolveLocalLlmConnection(connections, model) {
79
- const named = model ? connections.byModel?.[model] : undefined;
99
+ const wanted = namedLocalModel(model);
100
+ const named = wanted ? connections.byModel?.[wanted] : undefined;
80
101
  const connection = named ?? connections.default;
81
102
  if (!connection?.apiKey) {
82
- throw new Error(model
83
- ? `No local LLM gateway for model ${model}. Set runtime.llm.models["${model}"] in eap.config.json.`
84
- : 'No local LLM gateway. Set runtime.llm in eap.config.json.');
103
+ const available = Object.keys(connections.byModel ?? {}).sort();
104
+ const availableHint = available.length > 0 ? ` Available: ${available.join(', ')}.` : '';
105
+ if (wanted) {
106
+ throw new Error(`No local LLM gateway for model ${wanted}. Set runtime.llm.models["${wanted}"] in eap.config.json.${availableHint}`);
107
+ }
108
+ throw new Error(`No local LLM gateway. Set runtime.llm.models.default or runtime.llm.baseUrl in eap.config.json.${availableHint}`);
85
109
  }
86
110
  return connection;
87
111
  }
@@ -7,9 +7,9 @@
7
7
  * 3. 用返回的连接配置创建 HttpLLMProvider,发送请求
8
8
  *
9
9
  * 设计原则:
10
- * - Runtime 不存任何 LLM 凭证,全部从 Foundation 获取
10
+ * - 生产路径:Foundation `/models/select` 返回连接,本机 HTTP
11
+ * - select 失败或没有连接:走构造时注入的 fallback(dev 为 LocalLlmProvider / `resolveLocalLlmConnection`)
11
12
  * - 支持任意 OpenAI 兼容 API(OpenAI / DeepSeek / 本地 Ollama 等)
12
- * - 连接配置由 Foundation 统一管理,Runtime 无感知
13
13
  */
14
14
  import type { GraphMessage } from '../graph/contracts/executable-graph.js';
15
15
  import type { ILLMProvider, LLMNodeConfig, LLMResponse, LLMChunk } from '../graph/nodes/llm-node.js';
@@ -7,9 +7,9 @@
7
7
  * 3. 用返回的连接配置创建 HttpLLMProvider,发送请求
8
8
  *
9
9
  * 设计原则:
10
- * - Runtime 不存任何 LLM 凭证,全部从 Foundation 获取
10
+ * - 生产路径:Foundation `/models/select` 返回连接,本机 HTTP
11
+ * - select 失败或没有连接:走构造时注入的 fallback(dev 为 LocalLlmProvider / `resolveLocalLlmConnection`)
11
12
  * - 支持任意 OpenAI 兼容 API(OpenAI / DeepSeek / 本地 Ollama 等)
12
- * - 连接配置由 Foundation 统一管理,Runtime 无感知
13
13
  */
14
14
  import { AppError, ErrorCodes, mergeModelCallParams } from '@gt-fe/eap-contracts';
15
15
  import { EchoLLMProvider } from './echo-llm-provider.js';
@@ -52,7 +52,7 @@ export class RoutingLLMProvider {
52
52
  return {
53
53
  ...config,
54
54
  ...merged,
55
- model: selected.name,
55
+ model: selected.name || config.model,
56
56
  maxContextTokens: selected.maxContextTokens ?? config.maxContextTokens,
57
57
  };
58
58
  }
@@ -84,7 +84,7 @@ export class RoutingLLMProvider {
84
84
  catch {
85
85
  // fallback to local
86
86
  }
87
- return { name: config.model ?? 'echo', provider: 'local', connection: null };
87
+ return { name: config.model ?? '', provider: 'local', connection: null };
88
88
  }
89
89
  useFoundationInvoke() {
90
90
  return isFoundationModelsInvokeEnabled();
@@ -93,7 +93,6 @@ export class RoutingLLMProvider {
93
93
  if (this.useFoundationInvoke()) {
94
94
  return new FoundationModelsInvokeProvider(this.foundationUrl, selected.name, this.turnLog);
95
95
  }
96
- // 优先使用 Foundation 返回的连接配置
97
96
  if (selected.connection) {
98
97
  return new HttpLLMProvider({
99
98
  apiKey: selected.connection.apiKey ?? '',
@@ -103,16 +102,7 @@ export class RoutingLLMProvider {
103
102
  turnLog: this.turnLog,
104
103
  });
105
104
  }
106
- // Fallback: 兼容旧逻辑(直接读 env)—— 开发环境快速启动用
107
- if (process.env.OPENAI_API_KEY && selected.provider === 'openai') {
108
- return new HttpLLMProvider({
109
- apiKey: process.env.OPENAI_API_KEY,
110
- baseUrl: process.env.OPENAI_BASE_URL,
111
- defaultModel: selected.name,
112
- timeoutMs: DEFAULT_LLM_HTTP_TIMEOUT_MS,
113
- turnLog: this.turnLog,
114
- });
115
- }
105
+ // Foundation 未返回连接:与 LocalLlmProvider 同一把钥匙(由调用方注入 fallback)。
116
106
  return this.fallback;
117
107
  }
118
108
  }
@@ -116,7 +116,7 @@ async function findDigestMatch(items, expectedRaw) {
116
116
  }
117
117
  return undefined;
118
118
  }
119
- async function warnIfDigestMismatch(expectedRaw, actualHex, label, details) {
119
+ async function warnIfDigestMismatch(expectedRaw, actualHex, label, details, fail = false) {
120
120
  const expected = normalizeDigest(expectedRaw);
121
121
  if (!expected)
122
122
  return;
@@ -129,6 +129,13 @@ async function warnIfDigestMismatch(expectedRaw, actualHex, label, details) {
129
129
  actual,
130
130
  ...details,
131
131
  });
132
+ if (fail) {
133
+ throw preloadPackageCorrupt(`预装校验和不匹配: ${label}`, {
134
+ expected,
135
+ actual,
136
+ ...details,
137
+ });
138
+ }
132
139
  }
133
140
  async function pickLatestVersionDir(parent) {
134
141
  if (!(await pathExists(parent)))
@@ -373,6 +380,13 @@ export class FileSystemDeploymentSnapshot {
373
380
  actual: fileDigest,
374
381
  path: pkgFile,
375
382
  });
383
+ if (this.flags.requireChecksum || isRequireChecksum()) {
384
+ throw preloadPackageCorrupt('snapshot.sha256 与 .package.sha256 不一致', {
385
+ expected: expectedPkg,
386
+ actual: fileDigest,
387
+ path: pkgFile,
388
+ });
389
+ }
376
390
  }
377
391
  }
378
392
  }
@@ -394,7 +408,7 @@ export class FileSystemDeploymentSnapshot {
394
408
  throw preloadPackageCorrupt(`artifacts 指向的文件不存在: ${rel}`, { path: full });
395
409
  }
396
410
  const actual = await sha256OfFile(full);
397
- await warnIfDigestMismatch(expected, actual, `artifact ${rel}`, { path: full });
411
+ await warnIfDigestMismatch(expected, actual, `artifact ${rel}`, { path: full }, this.flags.requireChecksum || isRequireChecksum());
398
412
  }
399
413
  }
400
414
  async resolveAgentDir(agentCode, version) {
@@ -499,7 +513,7 @@ export class FileSystemDeploymentSnapshot {
499
513
  if (ref.sha256) {
500
514
  const matched = await findDigestMatch(existingManifests, ref.sha256);
501
515
  if (!matched) {
502
- await warnIfDigestMismatch(ref.sha256, await sha256OfFile(existingManifests[0].path), `tool ${ref.name}`, { path: existingManifests[0].path });
516
+ await warnIfDigestMismatch(ref.sha256, await sha256OfFile(existingManifests[0].path), `tool ${ref.name}`, { path: existingManifests[0].path }, this.flags.requireChecksum || isRequireChecksum());
503
517
  }
504
518
  chosen = matched ?? existingManifests[0];
505
519
  }
@@ -576,7 +590,7 @@ export class FileSystemDeploymentSnapshot {
576
590
  !isAuthorOverlay &&
577
591
  manifestPath.endsWith('.json')) {
578
592
  const actual = await sha256OfFile(manifestPath);
579
- await warnIfDigestMismatch(ref.sha256, actual, `skill ${ref.skillId}`, { path: manifestPath });
593
+ await warnIfDigestMismatch(ref.sha256, actual, `skill ${ref.skillId}`, { path: manifestPath }, this.flags.requireChecksum || isRequireChecksum());
580
594
  }
581
595
  const skillId = String(ref.skillId);
582
596
  return {
@@ -108,6 +108,8 @@ export declare class GraphLoader {
108
108
  definitionCacheSize(): number;
109
109
  private cloneCached;
110
110
  loadGraph(agentCode: string, options?: LoadAgentOptions): Promise<GraphDSL>;
111
+ /** Agent `models.default`;无清单或未声明则 undefined。 */
112
+ resolveDefaultModel(agentCode: string, version?: string): Promise<string | undefined>;
111
113
  private loadGraphFromSource;
112
114
  /**
113
115
  * Agent 基线系统提示词(manifest.prompts.system)。