@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.
- package/dist/public/assets/index-BR0s0cFI.js +16 -0
- package/dist/public/assets/index-BjsD-Ozu.css +1 -0
- package/dist/public/index.html +2 -2
- package/dist/src/agent-trace.js +42 -0
- package/dist/src/db.js +70 -0
- package/dist/src/engine.js +34 -4
- package/dist/src/flow-agent.js +64 -10
- package/dist/src/proposal.js +110 -60
- package/dist/src/runtime.js +59 -11
- package/dist/src/server.js +122 -14
- package/package.json +1 -1
- package/dist/public/assets/index-BYp134an.js +0 -16
- package/dist/public/assets/index-oZC2r1XL.css +0 -1
package/dist/src/proposal.js
CHANGED
|
@@ -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:
|
|
44
|
+
items: stageSchema(grounded),
|
|
43
45
|
},
|
|
44
46
|
},
|
|
45
47
|
});
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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,
|
|
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:${
|
|
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-${
|
|
421
|
-
const id = allocateId(matched?.id,
|
|
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
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
package/dist/src/runtime.js
CHANGED
|
@@ -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
|
-
|
|
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
|
});
|
package/dist/src/server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
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 {
|
|
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, {
|