@hmj-ai/cflow 1.3.7 → 1.3.9

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.
@@ -14,13 +14,15 @@ const capabilitySchema = (grounded) => ({
14
14
  type: 'object',
15
15
  additionalProperties: false,
16
16
  required: grounded
17
- ? ['kind', 'name', 'does', 'cfId', 'sourceQuote']
18
- : ['kind', 'name', 'does', 'cfId'],
17
+ ? ['kind', 'name', 'does', 'cfId', 'cond', 'routes', 'sourceQuote']
18
+ : ['kind', 'name', 'does', 'cfId', 'cond', 'routes'],
19
19
  properties: {
20
20
  kind: { type: 'string', enum: ['cf-call'] },
21
21
  name: { type: 'string' },
22
22
  does: { type: 'string' },
23
23
  cfId: { type: ['string', 'null'] },
24
+ cond: { type: 'null' },
25
+ routes: { type: 'array', maxItems: 0 },
24
26
  input: { type: ['string', 'null'] },
25
27
  output: { type: ['string', 'null'] },
26
28
  process: { type: ['string', 'null'] },
@@ -39,51 +41,53 @@ export const flowRevisionOutputSchema = (grounded = false) => ({
39
41
  stages: {
40
42
  type: 'array',
41
43
  maxItems: MAX_STAGES,
42
- items: capabilitySchema(grounded),
44
+ items: stageSchema(grounded),
43
45
  },
44
46
  },
45
47
  });
46
- const stageSchema = (grounded) => ({
47
- type: 'object',
48
- additionalProperties: false,
49
- required: grounded
50
- ? ['kind', 'name', 'does', 'cfId', 'cond', 'routes', 'sourceQuote']
51
- : ['kind', 'name', 'does', 'cfId', 'cond', 'routes'],
52
- properties: {
53
- kind: { type: 'string', enum: ['cf-call', 'branch'] },
54
- name: { type: 'string' },
55
- does: { type: ['string', 'null'] },
56
- cfId: { type: ['string', 'null'] },
57
- cond: { type: ['string', 'null'] },
58
- routes: {
59
- type: 'array',
60
- items: {
61
- type: 'object',
62
- additionalProperties: false,
63
- required: grounded
64
- ? ['caseId', 'condition', 'endsFlow', 'stages', 'sourceQuote']
65
- : ['caseId', 'condition', 'endsFlow', 'stages'],
66
- properties: {
67
- caseId: { type: 'string' },
68
- condition: { type: 'string' },
69
- endsFlow: { type: 'boolean' },
70
- sourceQuote: { type: ['string', 'null'] },
71
- stages: {
72
- type: 'array',
73
- minItems: 0,
74
- maxItems: 6,
75
- items: capabilitySchema(grounded),
48
+ function stageSchema(grounded) {
49
+ return {
50
+ type: 'object',
51
+ additionalProperties: false,
52
+ required: grounded
53
+ ? ['kind', 'name', 'does', 'cfId', 'cond', 'routes', 'sourceQuote']
54
+ : ['kind', 'name', 'does', 'cfId', 'cond', 'routes'],
55
+ properties: {
56
+ kind: { type: 'string', enum: ['cf-call', 'branch'] },
57
+ name: { type: 'string' },
58
+ does: { type: ['string', 'null'] },
59
+ cfId: { type: ['string', 'null'] },
60
+ cond: { type: ['string', 'null'] },
61
+ routes: {
62
+ type: 'array',
63
+ items: {
64
+ type: 'object',
65
+ additionalProperties: false,
66
+ required: grounded
67
+ ? ['caseId', 'condition', 'endsFlow', 'stages', 'sourceQuote']
68
+ : ['caseId', 'condition', 'endsFlow', 'stages'],
69
+ properties: {
70
+ caseId: { type: 'string' },
71
+ condition: { type: 'string' },
72
+ endsFlow: { type: 'boolean' },
73
+ sourceQuote: { type: ['string', 'null'] },
74
+ stages: {
75
+ type: 'array',
76
+ minItems: 0,
77
+ maxItems: 6,
78
+ items: capabilitySchema(grounded),
79
+ },
76
80
  },
77
81
  },
78
82
  },
83
+ input: { type: ['string', 'null'] },
84
+ output: { type: ['string', 'null'] },
85
+ process: { type: ['string', 'null'] },
86
+ sourceQuote: { type: ['string', 'null'] },
87
+ effects: { type: 'array', items: { type: 'object' } },
79
88
  },
80
- input: { type: ['string', 'null'] },
81
- output: { type: ['string', 'null'] },
82
- process: { type: ['string', 'null'] },
83
- sourceQuote: { type: ['string', 'null'] },
84
- effects: { type: 'array', items: { type: 'object' } },
85
- },
86
- });
89
+ };
90
+ }
87
91
  /**
88
92
  * `grounded` mirrors attachment mode: when a skill document is uploaded every
89
93
  * stage must carry a `sourceQuote`, so the schema demands it rather than only
@@ -360,11 +364,7 @@ const textOf = (value, fallback, max) => {
360
364
  const trimmed = value.trim();
361
365
  return trimmed ? trimmed.slice(0, max) : fallback;
362
366
  };
363
- /**
364
- * Rebuilds the current draft as a linear cf-call spine. The runtime may only
365
- * emit capability stages; branch/join/approval/onError are stripped here so a
366
- * revision cannot smuggle control structure into an existing Flow.
367
- */
367
+ /** Rebuilds the current draft from bounded capabilities and top-level branches. */
368
368
  export function applyFlowRevision(current, currentCfDrafts, rawStages, options) {
369
369
  const stages = (Array.isArray(rawStages) ? rawStages : []).slice(0, MAX_STAGES);
370
370
  if (!stages.length)
@@ -382,6 +382,7 @@ export function applyFlowRevision(current, currentCfDrafts, rawStages, options)
382
382
  const nodes = [];
383
383
  const edges = [];
384
384
  const cfDrafts = [];
385
+ const terminalRoutes = [];
385
386
  const allocateId = (preferred, fallback) => {
386
387
  let id = preferred && !claimed.has(preferred) ? preferred : fallback;
387
388
  let suffix = 2;
@@ -400,9 +401,7 @@ export function applyFlowRevision(current, currentCfDrafts, rawStages, options)
400
401
  return true;
401
402
  return cfById.get(node.cfRef.cfId)?.name?.trim() === name;
402
403
  });
403
- const capability = (stage, index) => {
404
- if (stage?.kind && stage.kind !== 'cf-call')
405
- throw new Error(`RUNTIME_REVISION_CONTROL_FORBIDDEN:${index}`);
404
+ const capability = (stage, fallback) => {
406
405
  const name = String(stage?.name ?? '')
407
406
  .trim()
408
407
  .slice(0, 80);
@@ -410,15 +409,15 @@ export function applyFlowRevision(current, currentCfDrafts, rawStages, options)
410
409
  .trim()
411
410
  .slice(0, 500);
412
411
  if (!name || !does)
413
- throw new Error(`RUNTIME_REVISION_STAGE_INVALID:${index}`);
412
+ throw new Error(`RUNTIME_REVISION_STAGE_INVALID:${fallback}`);
414
413
  const requestedId = stage?.cfId ? String(stage.cfId) : '';
415
414
  if (requestedId && !allowedIds.has(requestedId))
416
415
  throw new Error(`RUNTIME_REVISION_CF_UNKNOWN:${requestedId}`);
417
416
  const knownId = requestedId;
418
417
  const published = knownId ? catalog.find((item) => item.cfId === knownId) : undefined;
419
418
  const candidate = knownId ? cfById.get(knownId) : undefined;
420
- const matched = matchExisting(knownId || `unmatched-${index}`, name);
421
- const id = allocateId(matched?.id, `step-${index + 1}`);
419
+ const matched = matchExisting(knownId || `unmatched-${fallback}`, name);
420
+ const id = allocateId(matched?.id, fallback);
422
421
  const executor = matched?.kind === 'cf-call' ? matched.executor : runtimeId;
423
422
  const withMeta = (cfId, version) => ({
424
423
  id,
@@ -459,21 +458,72 @@ export function applyFlowRevision(current, currentCfDrafts, rawStages, options)
459
458
  });
460
459
  return withMeta(cfId, '1.0.0');
461
460
  };
462
- let previous = '$entry';
461
+ const connect = (from, to, when) => {
462
+ edges.push({ id: `edge-${edges.length + 1}`, from, to, ...(when ? { when } : {}) });
463
+ };
464
+ let frontier = ['$entry'];
463
465
  for (const [index, stage] of stages.entries()) {
464
- const node = capability(stage, index);
465
- nodes.push(node);
466
- edges.push({
467
- id: `edge-${edges.length + 1}`,
468
- from: previous,
469
- to: node.id,
466
+ if (!frontier.length)
467
+ throw new Error('RUNTIME_REVISION_AFTER_TERMINAL_BRANCH');
468
+ if (stage?.kind !== 'branch') {
469
+ const node = capability(stage, `step-${index + 1}`);
470
+ nodes.push(node);
471
+ frontier.forEach((from) => connect(from, node.id));
472
+ frontier = [node.id];
473
+ continue;
474
+ }
475
+ const routes = Array.isArray(stage.routes) ? stage.routes.slice(0, 8) : [];
476
+ if (routes.length < 2)
477
+ throw new Error('RUNTIME_REVISION_BRANCH_ROUTES_INVALID');
478
+ const cases = [];
479
+ const caseConditions = {};
480
+ const branchId = allocateId(undefined, `branch-${index + 1}`);
481
+ const branch = {
482
+ id: branchId,
483
+ kind: 'branch',
484
+ cond: { $get: String(stage.cond ?? 'route').trim() || 'route' },
485
+ cases,
486
+ caseConditions,
487
+ };
488
+ nodes.push(branch);
489
+ frontier.forEach((from) => connect(from, branch.id));
490
+ const tails = [];
491
+ routes.forEach((route, routeIndex) => {
492
+ const caseId = String(route?.caseId ?? `case-${routeIndex + 1}`).trim();
493
+ const condition = String(route?.condition ?? '')
494
+ .trim()
495
+ .slice(0, 500);
496
+ if (!caseId || !condition || cases.includes(caseId))
497
+ throw new Error('RUNTIME_REVISION_BRANCH_CASE_INVALID');
498
+ cases.push(caseId);
499
+ caseConditions[caseId] = condition;
500
+ const routeStages = Array.isArray(route?.stages) ? route.stages.slice(0, MAX_STAGES) : [];
501
+ if (typeof route?.endsFlow !== 'boolean')
502
+ throw new Error('RUNTIME_REVISION_BRANCH_ROUTE_END_INVALID');
503
+ if (route.endsFlow && routeStages.length)
504
+ throw new Error('RUNTIME_REVISION_BRANCH_ROUTE_TERMINAL_WITH_STAGES');
505
+ if (!routeStages.length && !route.endsFlow)
506
+ throw new Error('RUNTIME_REVISION_BRANCH_ROUTE_EMPTY');
507
+ if (!routeStages.length) {
508
+ terminalRoutes.push({ branchId: branch.id, caseId });
509
+ return;
510
+ }
511
+ let previous = branch.id;
512
+ routeStages.forEach((routeStage, stageIndex) => {
513
+ const node = capability(routeStage, `step-${index + 1}-${routeIndex + 1}-${stageIndex + 1}`);
514
+ nodes.push(node);
515
+ connect(previous, node.id, stageIndex === 0 ? { outcome: 'branch-case', caseId } : undefined);
516
+ previous = node.id;
517
+ });
518
+ tails.push(previous);
470
519
  });
471
- previous = node.id;
520
+ frontier = tails;
472
521
  }
473
522
  const previousOutput = current.nodes.find((node) => node.kind === 'output');
474
523
  const output = previousOutput ?? { id: 'output', kind: 'output', outputId: 'result' };
475
524
  nodes.push(output);
476
- edges.push({ id: `edge-${edges.length + 1}`, from: previous, to: output.id });
525
+ frontier.forEach((from) => connect(from, output.id));
526
+ terminalRoutes.forEach(({ branchId, caseId }) => connect(branchId, output.id, { outcome: 'branch-case', caseId }));
477
527
  const flowDraft = {
478
528
  ...current,
479
529
  revision: current.revision + 1,
@@ -393,7 +393,7 @@ export class RuntimeManager {
393
393
  }
394
394
  const executor = (id) => registry.register({
395
395
  id,
396
- execute: (task, input, signal, resources, effects, context) => this.executeProfile(profile, task, input, signal, resources, undefined, effects, context),
396
+ execute: (task, input, signal, resources, effects, context, trace) => this.executeProfile(profile, task, input, signal, resources, undefined, effects, context, undefined, trace),
397
397
  });
398
398
  executor(`${profile.id}@${profile.profileVersion}`);
399
399
  if (!exactOnly)
@@ -483,21 +483,27 @@ export class RuntimeManager {
483
483
  }
484
484
  throw new Error(`RUNTIME_BACKEND_UNSUPPORTED:${profile.backend}`);
485
485
  }
486
- async execute(id, task, input, signal, resources = [], outputSchema, context) {
486
+ async execute(id, task, input, signal, resources = [], outputSchema, context, trace) {
487
487
  const profile = this.profile(id);
488
488
  if (!profile)
489
489
  throw new Error(`UNKNOWN_EXECUTOR:${id}`);
490
- return this.executeProfile(profile, task, input, signal, resources, outputSchema, [], context);
490
+ return this.executeProfile(profile, task, input, signal, resources, outputSchema, [], context, undefined, trace);
491
491
  }
492
- async executeAnalysis(id, task, input, signal, options, outputSchema) {
492
+ async executeAnalysis(id, task, input, signal, options, outputSchema, trace) {
493
493
  const profile = this.profile(id);
494
494
  if (!profile)
495
495
  throw new Error(`UNKNOWN_EXECUTOR:${id}`);
496
- return this.executeProfile(profile, task, input, signal, [], outputSchema, [], undefined, options);
496
+ return this.executeProfile(profile, task, input, signal, [], outputSchema, [], undefined, options, trace);
497
497
  }
498
- async executeProfile(profile, task, input, signal, resources = [], outputSchema, effects = [], context, analysis) {
498
+ async executeProfile(profile, task, input, signal, resources = [], outputSchema, effects = [], context, analysis, trace) {
499
499
  if (!profile.enabled)
500
500
  throw new Error(`RUNTIME_DISABLED:${profile.id}`);
501
+ trace?.({
502
+ kind: 'stage',
503
+ title: '已准备 Agent 请求',
504
+ status: 'completed',
505
+ detail: `${profile.name} · ${profile.backend.toUpperCase()}`,
506
+ });
501
507
  if (profile.backend === 'builtin')
502
508
  return { task, input };
503
509
  const prompt = [
@@ -520,9 +526,9 @@ export class RuntimeManager {
520
526
  .filter(Boolean)
521
527
  .join('\n\n');
522
528
  const text = profile.backend === 'acp'
523
- ? await this.runAcp(profile, prompt, signal, effects, context, analysis)
529
+ ? await this.runAcp(profile, prompt, signal, effects, context, analysis, trace)
524
530
  : profile.backend === 'cli'
525
- ? await this.runCli(profile, prompt, signal, effects, context, analysis)
531
+ ? await this.runCli(profile, prompt, signal, effects, context, analysis, trace)
526
532
  : await Promise.reject(new Error(`RUNTIME_BACKEND_UNSUPPORTED:${profile.backend}`));
527
533
  return profile.outputMode === 'json' ? parseJsonOutput(text) : { content: text.trim() };
528
534
  }
@@ -742,7 +748,7 @@ export class RuntimeManager {
742
748
  const mode = canRun ? 'full' : canWrite ? 'write' : canRead ? 'read' : 'none';
743
749
  return profile.permissionArgs?.[mode] ?? [];
744
750
  }
745
- async runCli(profile, prompt, signal, effects = [], context, analysis) {
751
+ async runCli(profile, prompt, signal, effects = [], context, analysis, trace) {
746
752
  const cwd = analysis?.cwd ?? context?.workspaceRoot ?? profile.workingDirectory ?? process.cwd();
747
753
  const args = [
748
754
  ...profile.args,
@@ -751,6 +757,7 @@ export class RuntimeManager {
751
757
  ];
752
758
  const spec = this.launchSpec(profile, args, cwd, ['pipe', 'pipe', 'pipe'], effects);
753
759
  const child = launchProcess(spec);
760
+ trace?.({ kind: 'stage', title: '正在等待 Agent 响应', status: 'running' });
754
761
  const output = [];
755
762
  const errors = [];
756
763
  let outputBytes = 0;
@@ -796,6 +803,7 @@ export class RuntimeManager {
796
803
  const stderr = Buffer.concat(errors).toString('utf8').trim().slice(-800);
797
804
  if (result.code !== 0)
798
805
  throw new Error(`AGENT_CLI_EXITED:${result.code ?? result.childSignal ?? 'unknown'}${stderr ? `:${stderr}` : ''}`);
806
+ trace?.({ kind: 'stage', title: 'Agent 已返回结果', status: 'completed' });
799
807
  return Buffer.concat(output).toString('utf8');
800
808
  }
801
809
  finally {
@@ -803,7 +811,7 @@ export class RuntimeManager {
803
811
  terminate();
804
812
  }
805
813
  }
806
- async runAcp(profile, prompt, signal, effects = [], context, analysis) {
814
+ async runAcp(profile, prompt, signal, effects = [], context, analysis, trace) {
807
815
  const cwd = analysis?.cwd ?? context?.workspaceRoot ?? profile.workingDirectory ?? process.cwd();
808
816
  const spec = this.launchSpec(profile, profile.args, cwd, ['pipe', 'pipe', 'pipe'], effects);
809
817
  const child = launchProcess(spec);
@@ -824,6 +832,7 @@ export class RuntimeManager {
824
832
  throw new Error('ACP_SERVER_STDIO_UNAVAILABLE');
825
833
  const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
826
834
  let permissionDenied = false;
835
+ trace?.({ kind: 'stage', title: '正在连接 Agent 会话', status: 'running' });
827
836
  return await acpClient({ name: 'CFlow' })
828
837
  .onRequest(methods.client.session.requestPermission, async (ctx) => {
829
838
  const tool = ctx?.params?.toolCall ?? ctx?.toolCall ?? {};
@@ -851,7 +860,45 @@ export class RuntimeManager {
851
860
  });
852
861
  return ctx.buildSession(cwd).withSession(async (session) => {
853
862
  const promptResult = session.prompt(prompt);
854
- const text = await session.readText();
863
+ let text = '';
864
+ let analysisReported = false;
865
+ while (true) {
866
+ const update = await session.nextUpdate();
867
+ if (update.kind === 'stop')
868
+ break;
869
+ const item = update.update;
870
+ const tag = item?.sessionUpdate;
871
+ if (tag === 'agent_message_chunk' && item.content?.type === 'text')
872
+ text += item.content.text;
873
+ else if (tag === 'agent_thought_chunk' && !analysisReported) {
874
+ analysisReported = true;
875
+ trace?.({ kind: 'stage', title: 'Agent 正在分析', status: 'running' });
876
+ }
877
+ else if (tag === 'plan')
878
+ trace?.({
879
+ kind: 'plan',
880
+ title: 'Agent 制定了执行计划',
881
+ detail: item.entries?.map((entry) => entry.content).join(';'),
882
+ technical: item,
883
+ });
884
+ else if (tag === 'tool_call' || tag === 'tool_call_update')
885
+ trace?.({
886
+ kind: 'tool',
887
+ title: item.title ?? item.name ?? 'Agent 工具调用',
888
+ detail: item.status ? `状态:${item.status}` : undefined,
889
+ status: item.status === 'failed'
890
+ ? 'failed'
891
+ : item.status === 'completed'
892
+ ? 'completed'
893
+ : 'running',
894
+ technical: {
895
+ toolCallId: item.toolCallId,
896
+ input: item.rawInput,
897
+ output: item.rawOutput,
898
+ locations: item.locations,
899
+ },
900
+ });
901
+ }
855
902
  const response = await promptResult;
856
903
  if (response.stopReason !== 'end_turn') {
857
904
  const reason = String(response.stopReason).toUpperCase();
@@ -869,6 +916,7 @@ export class RuntimeManager {
869
916
  }
870
917
  if (Buffer.byteLength(text, 'utf8') > profile.maxOutputBytes)
871
918
  throw new Error('RUNTIME_OUTPUT_LIMIT_EXCEEDED');
919
+ trace?.({ kind: 'stage', title: 'Agent 已完成处理', status: 'completed' });
872
920
  return text;
873
921
  });
874
922
  });
@@ -4,6 +4,7 @@ import fastifyStatic from '@fastify/static';
4
4
  import fastifyMultipart from '@fastify/multipart';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { existsSync, realpathSync } from 'node:fs';
7
+ import { randomUUID } from 'node:crypto';
7
8
  import { cp, mkdir, rm } from 'node:fs/promises';
8
9
  import { dirname, join, sep } from 'node:path';
9
10
  import { Store } from './db.js';
@@ -29,6 +30,21 @@ export function createApp(store = new Store(), requestedWorkspaceRoot = process.
29
30
  ...[...testCatalogs.values()].flat(),
30
31
  ]);
31
32
  const scopeFlowDraft = (draft) => ({ ...draft, workspaceRoot });
33
+ const startInvocation = (input) => {
34
+ const id = input.id?.trim() || `agent-${randomUUID()}`;
35
+ const existing = store.agentInvocation(id);
36
+ if (!existing)
37
+ store.createAgentInvocation({
38
+ id,
39
+ kind: input.kind,
40
+ status: 'running',
41
+ runtimeId: input.runtimeId,
42
+ flowId: input.flowId,
43
+ messageId: input.messageId,
44
+ });
45
+ return id;
46
+ };
47
+ const traceFor = (id) => (event) => store.appendAgentTrace(id, event);
32
48
  const app = Fastify({ logger: true });
33
49
  app.register(fastifyMultipart, {
34
50
  // Do not silently truncate large skill bundles; files are streamed to disk.
@@ -249,6 +265,7 @@ export function createApp(store = new Store(), requestedWorkspaceRoot = process.
249
265
  const result = store.db.prepare('DELETE FROM flow_drafts WHERE id=?').run(req.params.id);
250
266
  if (!result.changes)
251
267
  return reply.code(404).send({ error: 'FLOW_DRAFT_NOT_FOUND' });
268
+ store.deleteAgentInvocationsForFlow(req.params.id);
252
269
  return { deleted: true };
253
270
  });
254
271
  const deletePublishedFlow = async (id, reply) => {
@@ -335,6 +352,9 @@ export function createApp(store = new Store(), requestedWorkspaceRoot = process.
335
352
  const runtimeId = body.runtimeId?.trim();
336
353
  const profile = runtimeId ? runtimes.profile(runtimeId) : undefined;
337
354
  const runtimeUsable = Boolean(runtimeId) && profile?.backend !== 'builtin';
355
+ const invocationId = runtimeUsable
356
+ ? startInvocation({ id: body.invocationId, kind: 'flow-proposal', runtimeId })
357
+ : undefined;
338
358
  // Attachment analysis needs a real agent runtime; the keyword fallback
339
359
  // below can only match already published capabilities.
340
360
  if (multipart && !runtimeUsable)
@@ -396,16 +416,21 @@ export function createApp(store = new Store(), requestedWorkspaceRoot = process.
396
416
  let response;
397
417
  try {
398
418
  response = multipart
399
- ? await runtimes.executeAnalysis(runtimeId, prompt, input, deadline, { cwd: multipart.root, allowedRoot: multipart.root }, flowProposalOutputSchema(true))
400
- : await runtimes.execute(runtimeId, prompt, input, deadline, [], flowProposalOutputSchema(false), { workspaceRoot });
419
+ ? await runtimes.executeAnalysis(runtimeId, prompt, input, deadline, { cwd: multipart.root, allowedRoot: multipart.root }, flowProposalOutputSchema(true), invocationId ? traceFor(invocationId) : undefined)
420
+ : await runtimes.execute(runtimeId, prompt, input, deadline, [], flowProposalOutputSchema(false), { workspaceRoot }, invocationId ? traceFor(invocationId) : undefined);
401
421
  }
402
422
  catch (error) {
403
423
  await discard();
424
+ if (invocationId)
425
+ store.setAgentInvocation(invocationId, 'failed', undefined, error instanceof Error ? error.message : String(error));
404
426
  throw error;
405
427
  }
406
428
  const proposal = response;
407
- if (!proposal || typeof proposal !== 'object' || !Array.isArray(proposal.stages))
429
+ if (!proposal || typeof proposal !== 'object' || !Array.isArray(proposal.stages)) {
430
+ if (invocationId)
431
+ store.setAgentInvocation(invocationId, 'failed', undefined, 'RUNTIME_PROPOSAL_INVALID');
408
432
  throw await failing('RUNTIME_PROPOSAL_INVALID');
433
+ }
409
434
  if (multipart) {
410
435
  const failures = collectGroundingFailures(proposal, multipart.contents);
411
436
  if (failures.length) {
@@ -418,11 +443,17 @@ export function createApp(store = new Store(), requestedWorkspaceRoot = process.
418
443
  })),
419
444
  }, 'flow proposal rejected: stages not grounded in the uploaded source');
420
445
  await discard();
421
- throw groundingError(failures, proposal);
446
+ const error = groundingError(failures, proposal);
447
+ if (invocationId)
448
+ store.setAgentInvocation(invocationId, 'failed', undefined, error.message);
449
+ throw error;
422
450
  }
423
451
  }
424
452
  const graph = buildProposalGraph(proposal.stages, { catalog, runtimeId });
425
- return asProposal(graph, String(proposal.flowName ?? objective), String(proposal.summary ?? 'Runtime 已生成可审阅的 Flow 草案。').slice(0, 1000));
453
+ const result = await asProposal(graph, String(proposal.flowName ?? objective), String(proposal.summary ?? 'Runtime 已生成可审阅的 Flow 草案。').slice(0, 1000), invocationId ? { invocationId } : {});
454
+ if (invocationId)
455
+ store.setAgentInvocation(invocationId, 'completed', result);
456
+ return result;
426
457
  }
427
458
  const matched = matchPublishedCapabilities(objective, catalog);
428
459
  if (!matched.length)
@@ -454,6 +485,13 @@ export function createApp(store = new Store(), requestedWorkspaceRoot = process.
454
485
  const health = await runtimes.health(runtimeId);
455
486
  if (health.status !== 'available')
456
487
  return { ...fallback, fallback: true };
488
+ const invocationId = startInvocation({
489
+ id: req.body.invocationId,
490
+ kind: 'flow-assistant',
491
+ runtimeId,
492
+ flowId: request.flowDraft.flowId,
493
+ messageId: req.body.messageId,
494
+ });
457
495
  const availableRuntimes = [];
458
496
  for (const item of runtimes.profiles()) {
459
497
  if (!item.enabled)
@@ -463,19 +501,48 @@ export function createApp(store = new Store(), requestedWorkspaceRoot = process.
463
501
  availableRuntimes.push({ id: item.id, name: item.name });
464
502
  }
465
503
  const grounded = Boolean(req.body.attachments?.length);
466
- const response = await runtimes.execute(runtimeId, flowAgentPrompt(message, grounded), flowAgentContext({ ...request, message }, availableRuntimes, catalog), AbortSignal.timeout(runtimes.settings().testTimeoutMs), [], flowAgentOutputSchema(grounded), { workspaceRoot });
467
- const normalized = normalizeAgentResponse(response);
468
- if (normalized.intent === 'answer')
469
- return { ...normalized, runtimeId };
470
- if (grounded)
471
- assertAttachmentGrounding(normalized, req.body.attachments ?? []);
472
- const revised = applyFlowRevision(request.flowDraft, request.cfDrafts ?? [], normalized.stages, { catalog, runtimeId });
504
+ let response;
505
+ try {
506
+ response = await runtimes.execute(runtimeId, flowAgentPrompt(message, grounded), flowAgentContext({ ...request, message }, availableRuntimes, catalog), AbortSignal.timeout(runtimes.settings().testTimeoutMs), [], flowAgentOutputSchema(grounded), { workspaceRoot }, traceFor(invocationId));
507
+ }
508
+ catch (error) {
509
+ store.setAgentInvocation(invocationId, 'failed', undefined, error instanceof Error ? error.message : String(error));
510
+ throw error;
511
+ }
512
+ let normalized;
513
+ try {
514
+ normalized = normalizeAgentResponse(response);
515
+ }
516
+ catch (error) {
517
+ store.setAgentInvocation(invocationId, 'failed', undefined, error instanceof Error ? error.message : String(error));
518
+ throw error;
519
+ }
520
+ if (normalized.intent === 'answer') {
521
+ const result = { ...normalized, runtimeId, invocationId };
522
+ store.setAgentInvocation(invocationId, 'completed', result);
523
+ return result;
524
+ }
525
+ let revised;
526
+ try {
527
+ if (grounded)
528
+ assertAttachmentGrounding(normalized, req.body.attachments ?? []);
529
+ revised = applyFlowRevision(request.flowDraft, request.cfDrafts ?? [], normalized.stages, {
530
+ catalog,
531
+ runtimeId,
532
+ });
533
+ }
534
+ catch (error) {
535
+ store.setAgentInvocation(invocationId, 'failed', undefined, error instanceof Error ? error.message : String(error));
536
+ throw error;
537
+ }
473
538
  store.db.transaction(() => {
474
539
  for (const draft of revised.cfDrafts)
475
540
  store.save('cf_drafts', draft.cfId, draft);
476
541
  store.save('flow_drafts', revised.flowDraft.flowId, revised.flowDraft);
477
542
  })();
478
- return { ...normalized, ...revised, runtimeId };
543
+ const result = { ...normalized, ...revised, runtimeId, invocationId };
544
+ store.setAgentInvocation(invocationId, 'completed', result);
545
+ return result;
479
546
  });
480
547
  app.get('/api/runs', async () => store.runs());
481
548
  app.get('/api/resources', async () => store.resourceProfiles());
@@ -584,7 +651,48 @@ export function createApp(store = new Store(), requestedWorkspaceRoot = process.
584
651
  const run = store.getRun(req.params.id);
585
652
  if (!run)
586
653
  return reply.code(404).send({ error: 'RUN_NOT_FOUND' });
587
- return { run, events: store.events(req.params.id) };
654
+ return {
655
+ run,
656
+ events: store.events(req.params.id),
657
+ invocations: store.agentInvocationsForRun(req.params.id),
658
+ };
659
+ });
660
+ app.get('/api/agent-invocations/:id', async (req, reply) => {
661
+ const invocation = store.agentInvocation(req.params.id);
662
+ if (!invocation)
663
+ return reply.code(404).send({ error: 'AGENT_INVOCATION_NOT_FOUND' });
664
+ return { invocation, events: store.agentTraceEvents(req.params.id) };
665
+ });
666
+ app.get('/api/agent-invocations/:id/events', async (req, reply) => {
667
+ if (!store.agentInvocation(req.params.id))
668
+ return reply.code(404).send({ error: 'AGENT_INVOCATION_NOT_FOUND' });
669
+ reply.raw.writeHead(200, {
670
+ 'content-type': 'text/event-stream',
671
+ 'cache-control': 'no-cache',
672
+ connection: 'keep-alive',
673
+ });
674
+ let sent = Number(req.headers['last-event-id'] ?? 0);
675
+ const flush = () => {
676
+ for (const event of store.agentTraceEvents(req.params.id).filter((item) => item.seq > sent)) {
677
+ reply.raw.write(`id: ${event.seq}\ndata: ${JSON.stringify(event)}\n\n`);
678
+ sent = event.seq;
679
+ }
680
+ const invocation = store.agentInvocation(req.params.id);
681
+ if (invocation && !['queued', 'running'].includes(invocation.status)) {
682
+ reply.raw.write(`event: complete\ndata: ${JSON.stringify(invocation)}\n\n`);
683
+ return true;
684
+ }
685
+ return false;
686
+ };
687
+ if (flush())
688
+ return reply.raw.end();
689
+ const timer = setInterval(() => {
690
+ if (flush()) {
691
+ clearInterval(timer);
692
+ reply.raw.end();
693
+ }
694
+ }, 100);
695
+ req.raw.on('close', () => clearInterval(timer));
588
696
  });
589
697
  app.get('/api/runs/:id/events', async (req, reply) => {
590
698
  reply.raw.writeHead(200, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hmj-ai/cflow",
3
- "version": "1.3.7",
3
+ "version": "1.3.9",
4
4
  "description": "以 Flow 为核心的本机多 Agent 编排工作台",
5
5
  "main": "dist/src/server.js",
6
6
  "bin": {