@hmj-ai/cflow 1.3.8 → 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.
@@ -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.8",
3
+ "version": "1.3.9",
4
4
  "description": "以 Flow 为核心的本机多 Agent 编排工作台",
5
5
  "main": "dist/src/server.js",
6
6
  "bin": {