@xdxer/dingtalk-agent 0.1.5-beta.8 → 0.1.5-beta.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/CHANGELOG.md +24 -0
- package/README.en.md +1 -0
- package/README.md +1 -0
- package/dist/bin/dingtalk-agent.js +115 -14
- package/dist/bin/dingtalk-agent.js.map +1 -1
- package/dist/src/agent-audit.js +108 -9
- package/dist/src/agent-audit.js.map +1 -1
- package/dist/src/development-workspace.js +37 -1
- package/dist/src/development-workspace.js.map +1 -1
- package/dist/src/dws.js +9 -0
- package/dist/src/dws.js.map +1 -1
- package/dist/src/multica-deploy.js +347 -105
- package/dist/src/multica-deploy.js.map +1 -1
- package/dist/src/multica-provider.js +151 -27
- package/dist/src/multica-provider.js.map +1 -1
- package/dist/src/multica-runtime-vocabulary.js +110 -0
- package/dist/src/multica-runtime-vocabulary.js.map +1 -0
- package/dist/src/promotion.js +2 -1
- package/dist/src/promotion.js.map +1 -1
- package/dist/src/schedule-plan.js +192 -24
- package/dist/src/schedule-plan.js.map +1 -1
- package/docs/AGENT-IN-PRODUCTION.md +255 -0
- package/docs/ARCHITECTURE.md +5 -1
- package/docs/PLATFORM-GUARDRAILS.md +188 -0
- package/docs/schemas/multica-deployment-plan.schema.json +3 -1
- package/docs/schemas/multica-deployment-receipt.schema.json +15 -1
- package/docs/schemas/multica-deployment-status.schema.json +6 -2
- package/docs/schemas/multica-workspace-inspection.schema.json +16 -0
- package/docs/schemas/multica-workspace-status.schema.json +2 -0
- package/docs/schemas/workspace-scaffold.schema.json +1 -0
- package/lab/project-workspace/fake-multica-provider.mjs +97 -7
- package/lab/project-workspace/multica-deploy.fixture.json +2 -2
- package/lab/project-workspace/multica-readonly.fixture.json +2 -2
- package/lab/project-workspace/project.fixture.json +1 -1
- package/lab/robot-eval/suite.json +1 -1
- package/package.json +5 -2
- package/skills/core/dta-agent-compose/SKILL.md +1 -1
- package/skills/core/dta-agent-compose/references/drive-and-schedules.md +22 -2
- package/skills/core/dta-basic-behavior/SKILL.md +1 -1
- package/skills/core/dta-basic-behavior/references/memory-and-evolution.md +1 -1
- package/skills/core/dta-people-group-memory/SKILL.md +4 -4
- package/skills/core/dta-people-group-memory/references/model.md +1 -1
- package/skills/platforms/multica-dingtalk/PLATFORM.md +2 -2
- package/skills/platforms/multica-dingtalk/dta-deploy-multica/SKILL.md +6 -5
- package/skills/platforms/multica-dingtalk/dta-deploy-multica/references/multica-deployment-contract.md +14 -4
- package/skills/platforms/multica-dingtalk/dta-ops-multica/SKILL.md +33 -7
- package/skills/platforms/multica-dingtalk/dta-ops-multica/scripts/multica_ext.py +118 -12
|
@@ -5,7 +5,8 @@ import { tmpdir } from 'node:os';
|
|
|
5
5
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
6
6
|
import { WORKSPACE_STATE_ROOT, inspectAgentProject, projectSkillRef, showDevelopmentWorkspace, } from './development-workspace.js';
|
|
7
7
|
import { digest, stableStringify } from './events.js';
|
|
8
|
-
import { MULTICA_EVIDENCE_ROOT, inspectMulticaWorkspace, planMulticaWorkspace, statusMulticaWorkspace, } from './multica-provider.js';
|
|
8
|
+
import { MULTICA_EVIDENCE_ROOT, emitProviderDiagnostic, inspectMulticaWorkspace, planMulticaWorkspace, sanitizeProviderEnv, statusMulticaWorkspace, } from './multica-provider.js';
|
|
9
|
+
import { allRuntimeVocabularies, commandInvokesDws, forbiddenSmokeTool, skillsLoadedInContent, vocabulariesForRuntimeProvider, } from './multica-runtime-vocabulary.js';
|
|
9
10
|
export const MULTICA_DEPLOYMENT_SCHEMA = 'dingtalk-agent/multica-deployment-receipt@1';
|
|
10
11
|
const MAX_DEPLOY_SKILLS = 32;
|
|
11
12
|
const MAX_SKILL_SUPPORTING_FILES = 128;
|
|
@@ -13,10 +14,12 @@ const deploymentWriteBudgets = new WeakMap();
|
|
|
13
14
|
class DeploymentFailure extends Error {
|
|
14
15
|
category;
|
|
15
16
|
ambiguous;
|
|
16
|
-
|
|
17
|
+
detail;
|
|
18
|
+
constructor(category, ambiguous = false, detail) {
|
|
17
19
|
super(category);
|
|
18
20
|
this.category = category;
|
|
19
21
|
this.ambiguous = ambiguous;
|
|
22
|
+
this.detail = detail;
|
|
20
23
|
}
|
|
21
24
|
}
|
|
22
25
|
export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
|
|
@@ -80,6 +83,7 @@ export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
|
|
|
80
83
|
if (!inspection.passed || inspection.resources.workspace?.id !== plan.workspaceId ||
|
|
81
84
|
inspection.resources.runtime?.id !== plan.runtimeId ||
|
|
82
85
|
inspection.resources.agent?.id !== plan.agentId ||
|
|
86
|
+
inspection.diff.includes('agent.runtime-drift') ||
|
|
83
87
|
stableStringify(assignedSkills) !== stableStringify(expectedSkills)) {
|
|
84
88
|
throw new Error('Multica workspace run scope 或 Skill assignment 与 Project 不一致;未创建 Issue');
|
|
85
89
|
}
|
|
@@ -92,6 +96,7 @@ export function runMulticaWorkspaceIssue(start, name, prompt, options = {}) {
|
|
|
92
96
|
const issueId = resourceId(issue.id, 'workspace run issue.id');
|
|
93
97
|
return readbackWorkspaceRun({
|
|
94
98
|
root: info.root, name, plan, issueId, runId, command, env, timeoutMs, calls, out: options.out,
|
|
99
|
+
runtimeProvider: inspection.resources.runtime?.provider || '',
|
|
95
100
|
});
|
|
96
101
|
}
|
|
97
102
|
/**
|
|
@@ -131,13 +136,43 @@ function readbackWorkspaceRun(ctx) {
|
|
|
131
136
|
}
|
|
132
137
|
const toolUses = messages.filter((item) => optionalString(item.type) === 'tool_use');
|
|
133
138
|
const toolNames = toolUses.map((item) => optionalString(item.tool)).filter(Boolean);
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
// Skill 装载与文件写入按 runtime 词汇解读(并集),并叠加 tool_result 的
|
|
140
|
+
// `**Skill loaded**` 正向证据 —— 与 smoke 判定同一套词汇,不再假定工具名叫 skill。
|
|
141
|
+
const vocabularies = ctx.runtimeProvider
|
|
142
|
+
? vocabulariesForRuntimeProvider(ctx.runtimeProvider) : [];
|
|
143
|
+
const active = vocabularies.length
|
|
144
|
+
? vocabularies : allRuntimeVocabularies();
|
|
145
|
+
const loadedSet = new Set();
|
|
146
|
+
const runBannerTools = new Set(active.flatMap((item) => [...item.skillResultTools]));
|
|
147
|
+
for (const item of messages) {
|
|
148
|
+
const type = optionalString(item.type);
|
|
149
|
+
const tool = optionalString(item.tool);
|
|
150
|
+
if (type === 'tool_use') {
|
|
151
|
+
const message = { tool, input: smokeToolInput(item) };
|
|
152
|
+
for (const vocabulary of active) {
|
|
153
|
+
const name = vocabulary.skillLoadFromToolUse(message);
|
|
154
|
+
if (name)
|
|
155
|
+
loadedSet.add(name);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (type === 'tool_result' && runBannerTools.has(tool)) {
|
|
159
|
+
for (const name of skillsLoadedInContent(optionalString(item.content)))
|
|
160
|
+
loadedSet.add(name);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const loadedSkills = [...loadedSet].sort();
|
|
136
164
|
const texts = messages.filter((item) => optionalString(item.type) === 'text')
|
|
137
165
|
.map((item) => optionalString(item.content)).filter(Boolean);
|
|
138
|
-
const replyWrites = toolUses.
|
|
139
|
-
|
|
140
|
-
|
|
166
|
+
const replyWrites = toolUses.flatMap((item) => {
|
|
167
|
+
const message = {
|
|
168
|
+
tool: optionalString(item.tool), input: smokeToolInput(item),
|
|
169
|
+
};
|
|
170
|
+
return active.map((vocabulary) => vocabulary.writeEvidence(message))
|
|
171
|
+
.filter((write) => Boolean(write))
|
|
172
|
+
// 裸文件名与 ./、绝对路径等价(平台常见写法,与 smoke 白名单同一课)。
|
|
173
|
+
.filter((write) => write.path === 'reply.md' || write.path.endsWith('/reply.md'))
|
|
174
|
+
.map((write) => write.content);
|
|
175
|
+
}).filter(Boolean);
|
|
141
176
|
const answer = safeRemoteOutput(replyWrites.at(-1) || texts.at(-1) || '', 64 * 1024);
|
|
142
177
|
const issueReadback = jsonObject(runProvider(command, scopedArgs({ profile: plan.profile, expected: { workspaceId: plan.workspaceId } }, [
|
|
143
178
|
'issue', 'get', issueId, '--output', 'json',
|
|
@@ -265,6 +300,7 @@ export function statusMulticaWorkspaceRun(start, name, options = {}) {
|
|
|
265
300
|
}
|
|
266
301
|
export function planMulticaDeployment(start, name, options = {}) {
|
|
267
302
|
const action = options.action || 'apply';
|
|
303
|
+
const rebindRuntime = Boolean(options.rebindRuntime) && action === 'apply';
|
|
268
304
|
const env = options.env || process.env;
|
|
269
305
|
const cliVersion = options.cliVersion || '';
|
|
270
306
|
const info = inspectAgentProject(start, cliVersion);
|
|
@@ -273,12 +309,21 @@ export function planMulticaDeployment(start, name, options = {}) {
|
|
|
273
309
|
const readPlan = planMulticaWorkspace(info.root, name, { env, cliVersion });
|
|
274
310
|
const blocking = [...readPlan.blocking];
|
|
275
311
|
let inspectionId = '';
|
|
312
|
+
let previousRuntimeId = '';
|
|
276
313
|
try {
|
|
277
314
|
const status = statusMulticaWorkspace(info.root, name, cliVersion);
|
|
278
315
|
inspectionId = status.inspectionId;
|
|
279
316
|
if (!status.readyForApply || !status.evidenceIntegrity || !status.stateMatched) {
|
|
280
317
|
blocking.push(...status.failures.map((item) => `inspection.${item}`));
|
|
281
318
|
}
|
|
319
|
+
// Runtime 归属漂移只挡「普通 apply」:没有显式 --rebind-runtime 时不允许悄悄把
|
|
320
|
+
// Agent 搬到新 runtime。retire 是收敛动作,不受漂移阻挡(否则 deploy 与 retire
|
|
321
|
+
// 会被同一把锁一起锁死)。
|
|
322
|
+
if (status.diff.includes('agent.runtime-drift')) {
|
|
323
|
+
previousRuntimeId = status.observedAgentRuntimeId || '';
|
|
324
|
+
if (action === 'apply' && !rebindRuntime)
|
|
325
|
+
blocking.push('agent.runtime-drift');
|
|
326
|
+
}
|
|
282
327
|
}
|
|
283
328
|
catch {
|
|
284
329
|
blocking.push('inspection.invalid');
|
|
@@ -287,26 +332,30 @@ export function planMulticaDeployment(start, name, options = {}) {
|
|
|
287
332
|
blocking.push('inspection.missing');
|
|
288
333
|
if (action === 'retire' && !readPlan.expected.agentId)
|
|
289
334
|
blocking.push('agent-id.missing');
|
|
335
|
+
if (rebindRuntime && !readPlan.expected.agentId)
|
|
336
|
+
blocking.push('rebind.agent-id-missing');
|
|
290
337
|
const expectedSkills = source.skills.map((skill) => ({
|
|
291
338
|
name: skill.name, hash: skill.hash, files: skill.files.length + 1,
|
|
292
339
|
}));
|
|
293
340
|
const operations = action === 'retire'
|
|
294
341
|
? ['scope.reinspect', 'agent.archive', 'agent.readback', 'receipt.persist']
|
|
295
342
|
: [
|
|
296
|
-
'scope.reinspect', 'skills.readback', 'skills.reconcile',
|
|
343
|
+
'scope.reinspect', 'skills.readback', 'skills.reconcile',
|
|
344
|
+
...(rebindRuntime ? ['agent.rebind-runtime'] : []), 'agent.reconcile',
|
|
297
345
|
'skills.assign-exact', 'provider.readback', 'load-smoke.create',
|
|
298
346
|
'load-smoke.recover-if-unstarted', 'load-smoke.readback', 'receipt.persist',
|
|
299
347
|
];
|
|
300
348
|
const writeBudget = action === 'retire'
|
|
301
349
|
? { forward: 1, rollback: 0 }
|
|
302
350
|
: {
|
|
303
|
-
|
|
351
|
+
// +3 on top of the base 5: bounded skills-not-visible smoke retries (cold sandbox).
|
|
352
|
+
forward: source.skills.length * (1 + MAX_SKILL_SUPPORTING_FILES * 2) + 8,
|
|
304
353
|
rollback: source.skills.length * (1 + MAX_SKILL_SUPPORTING_FILES * 2) + 3,
|
|
305
354
|
};
|
|
306
355
|
const planId = `multica_deploy_plan_${digest(stableStringify({
|
|
307
356
|
action, workspace: name, profile: readPlan.profile, inspectionId,
|
|
308
357
|
desiredHash: workspace.desiredHash, deploymentHash: source.deploymentHash,
|
|
309
|
-
expected: readPlan.expected, operations, writeBudget,
|
|
358
|
+
expected: readPlan.expected, operations, writeBudget, rebindRuntime, previousRuntimeId,
|
|
310
359
|
blocking: [...new Set(blocking)].sort(),
|
|
311
360
|
})).slice(0, 24)}`;
|
|
312
361
|
return {
|
|
@@ -327,7 +376,9 @@ export function planMulticaDeployment(start, name, options = {}) {
|
|
|
327
376
|
skills: expectedSkills,
|
|
328
377
|
definitionHash: source.definitionHash,
|
|
329
378
|
instructionsHash: source.instructionsHash,
|
|
379
|
+
...(rebindRuntime ? { previousRuntimeId } : {}),
|
|
330
380
|
},
|
|
381
|
+
rebindRuntime,
|
|
331
382
|
operations,
|
|
332
383
|
writeBudget,
|
|
333
384
|
blocking: [...new Set(blocking)].sort(),
|
|
@@ -348,9 +399,12 @@ export function applyMulticaDeployment(start, name, options = {}, action = 'appl
|
|
|
348
399
|
throw new Error('dta deploy 必须传 dry-run 返回的 --plan-id');
|
|
349
400
|
const env = options.env || process.env;
|
|
350
401
|
const cliVersion = options.cliVersion || '';
|
|
351
|
-
const
|
|
402
|
+
const rebindRuntime = Boolean(options.rebindRuntime) && action === 'apply';
|
|
403
|
+
const plan = planMulticaDeployment(start, name, { action, env, cliVersion, rebindRuntime });
|
|
352
404
|
if (options.planId !== plan.planId) {
|
|
353
|
-
throw new Error(`deploy plan 已失效;期望当前 planId ${plan.planId}
|
|
405
|
+
throw new Error(`deploy plan 已失效;期望当前 planId ${plan.planId}。` +
|
|
406
|
+
'plan 对中间状态敏感:dry-run 之后、apply 之前不要插入 workspace inspect 等操作;' +
|
|
407
|
+
'重跑 --dry-run 取新 planId 后立即 apply');
|
|
354
408
|
}
|
|
355
409
|
if (plan.blocking.length)
|
|
356
410
|
throw new Error(`deploy 前置条件不完整: ${plan.blocking.join(', ')}`);
|
|
@@ -368,7 +422,12 @@ export function applyMulticaDeployment(start, name, options = {}, action = 'appl
|
|
|
368
422
|
preflight.profile !== plan.profile ||
|
|
369
423
|
preflight.expected.workspaceId !== plan.expected.workspaceId ||
|
|
370
424
|
preflight.expected.runtimeId !== plan.expected.runtimeId) {
|
|
371
|
-
throw new Error('deploy scope/readback 与冻结 plan 不一致;未执行任何远端写入'
|
|
425
|
+
throw new Error('deploy scope/readback 与冻结 plan 不一致;未执行任何远端写入' +
|
|
426
|
+
'(dry-run 与 apply 之间若插入过 inspect 或远端有变化,重跑 --dry-run 后立即 apply)');
|
|
427
|
+
}
|
|
428
|
+
if (action === 'apply' && !rebindRuntime && preflight.diff.includes('agent.runtime-drift')) {
|
|
429
|
+
throw new Error('deploy 前置条件不完整: agent.runtime-drift' +
|
|
430
|
+
'(Agent 当前绑定在其他 runtime;显式迁移用 --rebind-runtime,退役用 --retire)');
|
|
372
431
|
}
|
|
373
432
|
appendInspectionCalls(preflight, 'preflight', calls);
|
|
374
433
|
const operationId = `multica_operation_${Date.now()}_${randomUUID().slice(0, 12)}`;
|
|
@@ -401,7 +460,7 @@ export function applyMulticaDeployment(start, name, options = {}, action = 'appl
|
|
|
401
460
|
operation.calls = calls;
|
|
402
461
|
operation.failures = [failure.category, ...rollbackReport.failures];
|
|
403
462
|
operation.updatedAt = new Date().toISOString();
|
|
404
|
-
receipt = failureReceipt(info.root, workspace, source, plan, preflight, operation, calls, rollbackReport, status, failure.category);
|
|
463
|
+
receipt = failureReceipt(info.root, workspace, source, plan, preflight, operation, calls, rollbackReport, status, failure.category, failure.detail);
|
|
405
464
|
}
|
|
406
465
|
finally {
|
|
407
466
|
rmSync(temp, { recursive: true, force: true });
|
|
@@ -429,6 +488,7 @@ function executeApply(root, workspace, source, plan, preflight, operation, comma
|
|
|
429
488
|
calls: [...calls],
|
|
430
489
|
agent: { ...previous.agent, action: 'noop' },
|
|
431
490
|
skills: previous.skills.map((skill) => ({ ...skill, action: 'noop' })),
|
|
491
|
+
failureDetails: Array.isArray(previous.failureDetails) ? previous.failureDetails : [],
|
|
432
492
|
evidencePath: receiptRelativePath(workspace.name, receiptId),
|
|
433
493
|
remoteWrite: false,
|
|
434
494
|
sideEffect: 'multica-read',
|
|
@@ -477,9 +537,12 @@ function executeApply(root, workspace, source, plan, preflight, operation, comma
|
|
|
477
537
|
throw new DeploymentFailure('readback.deployment-mismatch');
|
|
478
538
|
}
|
|
479
539
|
const observedHash = deploymentObservedHash(finalSnapshot, readback.inspectionId);
|
|
480
|
-
const smoke = createAndReadSmoke(root, plan, source, agentId, command, env, timeoutMs, smokeTimeoutMs, noWait, calls);
|
|
540
|
+
const smoke = createAndReadSmoke(root, plan, source, agentId, readback.resources.runtime?.provider || '', command, env, timeoutMs, smokeTimeoutMs, noWait, calls);
|
|
541
|
+
// skills-not-visible 是冷沙箱竞态(推送的 Skill 尚未物化),不是真实部署失败:落
|
|
542
|
+
// verifying 而非 failed,避免把一次正常部署误判成失败、把人推去改本体换模板。
|
|
481
543
|
const status = smoke.status === 'passed'
|
|
482
|
-
? 'ready' : smoke.status === 'pending' ? 'verifying'
|
|
544
|
+
? 'ready' : smoke.status === 'pending' ? 'verifying'
|
|
545
|
+
: smoke.classification === 'skills-not-visible' ? 'verifying' : 'failed';
|
|
483
546
|
const receiptId = `multica_receipt_${digest(stableStringify({
|
|
484
547
|
operationId: operation.operationId, planId: plan.planId, observedHash,
|
|
485
548
|
smoke: { markerHash: smoke.markerHash, issueId: smoke.issueId, taskId: smoke.taskId },
|
|
@@ -516,6 +579,13 @@ function executeApply(root, workspace, source, plan, preflight, operation, comma
|
|
|
516
579
|
smoke,
|
|
517
580
|
rollback: emptyRollback(false),
|
|
518
581
|
failures: [...smoke.failures],
|
|
582
|
+
failureDetails: [...smoke.failureDetails],
|
|
583
|
+
...(plan.rebindRuntime && plan.expected.previousRuntimeId ? {
|
|
584
|
+
rebind: {
|
|
585
|
+
fromRuntimeId: plan.expected.previousRuntimeId,
|
|
586
|
+
toRuntimeId: plan.expected.runtimeId,
|
|
587
|
+
},
|
|
588
|
+
} : {}),
|
|
519
589
|
evidencePath: receiptRelativePath(workspace.name, receiptId),
|
|
520
590
|
remoteRead: true,
|
|
521
591
|
remoteWrite: calls.some((call) => call.kind === 'write'),
|
|
@@ -570,6 +640,7 @@ function retireDeployment(root, workspace, source, plan, preflight, operation, c
|
|
|
570
640
|
smoke: emptySmoke('not-run', source.skills.map((item) => item.name)),
|
|
571
641
|
rollback: emptyRollback(false),
|
|
572
642
|
failures: [],
|
|
643
|
+
failureDetails: [],
|
|
573
644
|
evidencePath: receiptRelativePath(workspace.name, receiptId),
|
|
574
645
|
remoteRead: true,
|
|
575
646
|
remoteWrite: calls.some((call) => call.kind === 'write'),
|
|
@@ -784,7 +855,7 @@ function upsertSkillFile(root, plan, skillId, path, contentFile, command, env, t
|
|
|
784
855
|
'--content-file', contentFile, '--output', 'json',
|
|
785
856
|
]), `skill.file.upsert:${path}`, 'write', root, env, timeoutMs, calls);
|
|
786
857
|
}
|
|
787
|
-
function createAndReadSmoke(root, plan, source, agentId, command, env, timeoutMs, smokeTimeoutMs, noWait, calls) {
|
|
858
|
+
function createAndReadSmoke(root, plan, source, agentId, runtimeProvider, command, env, timeoutMs, smokeTimeoutMs, noWait, calls) {
|
|
788
859
|
const marker = `DTA-MULTICA-LOAD-${randomUUID()}`;
|
|
789
860
|
const requiredSkills = source.skills.map((item) => item.name).sort();
|
|
790
861
|
const expectedResponse = JSON.stringify({
|
|
@@ -796,14 +867,15 @@ function createAndReadSmoke(root, plan, source, agentId, command, env, timeoutMs
|
|
|
796
867
|
'DTA_MULTICA_LOAD_SMOKE@1', `marker=${marker}`,
|
|
797
868
|
`required_skills=${JSON.stringify(requiredSkills)}`,
|
|
798
869
|
'This is a deployment control-plane verification task.',
|
|
799
|
-
'
|
|
870
|
+
'Load every required skill above through the Host skill mechanism, in the listed order.',
|
|
800
871
|
'Do not call DWS, network, or business tools. Only the Host Issue control-plane calls needed to read this Issue and persist the exact result are exempt.',
|
|
801
|
-
`
|
|
872
|
+
`Post exactly this JSON as an Issue comment (and nothing else): ${expectedResponse}`,
|
|
802
873
|
].join('\n'),
|
|
803
874
|
'--assignee-id', agentId, '--output', 'json',
|
|
804
875
|
]), 'smoke.issue.create', 'write', root, env, timeoutMs, calls).stdout, 'smoke issue');
|
|
805
876
|
const issueId = resourceId(issue.id, 'smoke issue.id');
|
|
806
|
-
|
|
877
|
+
const read = () => readSmoke(root, plan, marker, issueId, agentId, requiredSkills, runtimeProvider, command, env, timeoutMs, calls);
|
|
878
|
+
let smoke = read();
|
|
807
879
|
// Multica persists the Issue before attempting the initial agent enqueue. If
|
|
808
880
|
// that enqueue loses a transient readiness race, the API still returns a
|
|
809
881
|
// valid assigned todo Issue but issue runs remains empty forever. Recover
|
|
@@ -812,19 +884,65 @@ function createAndReadSmoke(root, plan, source, agentId, command, env, timeoutMs
|
|
|
812
884
|
// duplicated. The mention route is used instead of issue rerun because it is
|
|
813
885
|
// the platform's normal first-run trigger and does not require a prior task.
|
|
814
886
|
if (smoke.status === 'pending' && !smoke.taskId) {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
887
|
+
try {
|
|
888
|
+
runProvider(command, scopedArgs(plan, [
|
|
889
|
+
'issue', 'comment', 'add', issueId,
|
|
890
|
+
'--content', `[@DTA load smoke](mention://agent/${agentId}) Execute the deployment verification exactly as specified in this Issue description.`,
|
|
891
|
+
'--output', 'json',
|
|
892
|
+
]), 'smoke.issue.recover', 'write', root, env, timeoutMs, calls);
|
|
893
|
+
smoke = read();
|
|
894
|
+
}
|
|
895
|
+
catch {
|
|
896
|
+
// 与重试 mention 同一不变量:部署本体已回读验证,一次评论写入的抖动只让 smoke
|
|
897
|
+
// 停在 pending(收敛为 verifying,由 --status 收口),不允许升级为回滚。
|
|
898
|
+
}
|
|
821
899
|
}
|
|
822
900
|
if (noWait)
|
|
823
901
|
return smoke;
|
|
824
902
|
const started = Date.now();
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
903
|
+
const pollToTerminal = () => {
|
|
904
|
+
while (smoke.status === 'pending' && Date.now() - started < smokeTimeoutMs) {
|
|
905
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2_000);
|
|
906
|
+
smoke = read();
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
pollToTerminal();
|
|
910
|
+
// 冷沙箱竞态:推送的 Skill 要数分钟才对运行时可见,任务报 `Skill not found` 不是
|
|
911
|
+
// 真失败。在预算内用 mention 触发新 task(新沙箱有机会看到新 Skill),有限次重试。
|
|
912
|
+
// 默认 30s 预算下基本轮不到重试;--wait / --smoke-timeout 放宽后才生效。
|
|
913
|
+
const retryDelayMs = Math.min(20_000, Math.max(500, Math.floor(smokeTimeoutMs / 12)));
|
|
914
|
+
let retries = 0;
|
|
915
|
+
while (smoke.status === 'failed' && smoke.classification === 'skills-not-visible' &&
|
|
916
|
+
retries < 3 && Date.now() - started < smokeTimeoutMs) {
|
|
917
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, retryDelayMs);
|
|
918
|
+
try {
|
|
919
|
+
runProvider(command, scopedArgs(plan, [
|
|
920
|
+
'issue', 'comment', 'add', issueId,
|
|
921
|
+
'--content', `[@DTA load smoke](mention://agent/${agentId}) Skills were not yet visible in the sandbox; re-run the deployment verification exactly as specified in this Issue description.`,
|
|
922
|
+
'--output', 'json',
|
|
923
|
+
]), 'smoke.issue.retry', 'write', root, env, timeoutMs, calls);
|
|
924
|
+
}
|
|
925
|
+
catch {
|
|
926
|
+
// 一次评论写入的抖动不允许升级为对已验证部署的回滚:失败已记入 calls,
|
|
927
|
+
// 停止重试并保持当前 smoke 判定(skills-not-visible → verifying)。
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
retries += 1;
|
|
931
|
+
// mention 触发的新 run 在平台上是异步创建的:只有当最新 run 的 taskId 变化后
|
|
932
|
+
// 才重新判定;等不到新 run 就停手——绝不因为还读到旧终态 run 而重复 mention。
|
|
933
|
+
const staleTaskId = smoke.taskId;
|
|
934
|
+
let sawNewRun = false;
|
|
935
|
+
while (Date.now() - started < smokeTimeoutMs) {
|
|
936
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2_000);
|
|
937
|
+
smoke = read();
|
|
938
|
+
if (smoke.taskId && smoke.taskId !== staleTaskId) {
|
|
939
|
+
sawNewRun = true;
|
|
940
|
+
break;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (!sawNewRun)
|
|
944
|
+
break;
|
|
945
|
+
pollToTerminal();
|
|
828
946
|
}
|
|
829
947
|
if (smoke.status === 'pending' && !smoke.taskDiagnostic) {
|
|
830
948
|
const detail = smoke.taskId
|
|
@@ -834,55 +952,180 @@ function createAndReadSmoke(root, plan, source, agentId, command, env, timeoutMs
|
|
|
834
952
|
}
|
|
835
953
|
return smoke;
|
|
836
954
|
}
|
|
837
|
-
|
|
955
|
+
/**
|
|
956
|
+
* Judge a smoke run from positive evidence instead of a tool-name whitelist. The old judge
|
|
957
|
+
* required every tool_use to match hardcoded `skill`/`write`/`bash` shapes, so a healthy
|
|
958
|
+
* cloud runtime emitting `read_file`/`terminal` could never reach ready — and one extra
|
|
959
|
+
* harmless tool failed the whole trace. Now: each required skill needs load evidence
|
|
960
|
+
* (vocabulary tool_use or host-generated `**Skill loaded**` tool_result), the response is
|
|
961
|
+
* taken from the Issue comments themselves first (platform readback, vocabulary-independent),
|
|
962
|
+
* and only the explicit dws denylist is fatal. Unknown tool names are recorded, not judged.
|
|
963
|
+
*/
|
|
964
|
+
function readSmoke(root, plan, marker, issueId, agentId, requiredSkills, runtimeProvider, command, env, timeoutMs, calls) {
|
|
965
|
+
const provider = runtimeProvider || '';
|
|
966
|
+
const registered = vocabulariesForRuntimeProvider(provider);
|
|
967
|
+
const active = registered.length
|
|
968
|
+
? registered : allRuntimeVocabularies();
|
|
969
|
+
const vocabularyIds = active.map((item) => item.id);
|
|
970
|
+
const base = {
|
|
971
|
+
...emptySmoke('pending', requiredSkills), marker, markerHash: sha256(marker), issueId,
|
|
972
|
+
runtimeProvider: provider, vocabularyIds,
|
|
973
|
+
};
|
|
838
974
|
const runs = jsonArray(runProvider(command, scopedArgs(plan, ['issue', 'runs', issueId, '--full-id', '--output', 'json']), 'smoke.issue.runs', 'read', root, env, timeoutMs, calls).stdout, 'smoke runs');
|
|
839
975
|
const candidates = runs.filter((item) => !optionalString(item.agent_id) ||
|
|
840
976
|
optionalString(item.agent_id) === agentId);
|
|
841
977
|
const run = candidates.sort((a, b) => optionalString(b.created_at).localeCompare(optionalString(a.created_at)))[0];
|
|
842
978
|
if (!run)
|
|
843
|
-
return
|
|
844
|
-
...emptySmoke('pending', requiredSkills), marker, markerHash: sha256(marker), issueId,
|
|
845
|
-
};
|
|
979
|
+
return base;
|
|
846
980
|
const taskId = resourceId(run.id, 'smoke task.id');
|
|
847
981
|
const taskStatus = optionalString(run.status);
|
|
848
982
|
if (!['completed', 'failed', 'cancelled'].includes(taskStatus))
|
|
849
983
|
return {
|
|
850
|
-
...
|
|
984
|
+
...base, taskId, taskStatus,
|
|
851
985
|
};
|
|
852
986
|
const messages = jsonArray(runProvider(command, scopedArgs(plan, [
|
|
853
987
|
'issue', 'run-messages', taskId, '--issue', issueId, '--output', 'json',
|
|
854
988
|
]), 'smoke.issue.messages', 'read', root, env, timeoutMs, calls).stdout, 'smoke messages');
|
|
855
989
|
const toolUses = messages.filter((item) => optionalString(item.type) === 'tool_use');
|
|
856
|
-
const
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
990
|
+
const toolNames = [...new Set(toolUses.map((item) => optionalString(item.tool))
|
|
991
|
+
.filter(Boolean))].sort();
|
|
992
|
+
// Skill-load evidence: a native skill tool_use (the host mechanism itself), or the
|
|
993
|
+
// host-generated `**Skill loaded**` banner in a skill-capable tool's tool_result. Two
|
|
994
|
+
// things are deliberately NOT evidence: model text (a reply can claim anything), and
|
|
995
|
+
// load ATTEMPTS — a failed cold-sandbox load emits the same "Loading skill" tool_use
|
|
996
|
+
// before its error result, and a terminal/bash result echoes whatever the model printed.
|
|
997
|
+
const evidence = new Set();
|
|
998
|
+
const attempted = new Set();
|
|
999
|
+
const evidenceTools = new Set();
|
|
1000
|
+
const bannerTools = new Set(active.flatMap((item) => [...item.skillResultTools]));
|
|
1001
|
+
for (const item of messages) {
|
|
1002
|
+
const type = optionalString(item.type);
|
|
1003
|
+
const tool = optionalString(item.tool);
|
|
1004
|
+
if (type === 'tool_use') {
|
|
1005
|
+
const message = { tool, input: smokeToolInput(item) };
|
|
1006
|
+
for (const vocabulary of active) {
|
|
1007
|
+
const name = vocabulary.skillLoadFromToolUse(message);
|
|
1008
|
+
if (name) {
|
|
1009
|
+
evidence.add(name);
|
|
1010
|
+
evidenceTools.add(tool);
|
|
1011
|
+
}
|
|
1012
|
+
const attempt = vocabulary.skillAttemptFromToolUse(message);
|
|
1013
|
+
if (attempt) {
|
|
1014
|
+
attempted.add(attempt);
|
|
1015
|
+
evidenceTools.add(tool);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
else if (type === 'tool_result' && bannerTools.has(tool)) {
|
|
1020
|
+
for (const name of skillsLoadedInContent(optionalString(item.content))) {
|
|
1021
|
+
evidence.add(name);
|
|
1022
|
+
evidenceTools.add(tool);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const loadedSkills = requiredSkills.filter((name) => evidence.has(name)).sort();
|
|
1027
|
+
const skillEvidencePassed = loadedSkills.length === requiredSkills.length;
|
|
1028
|
+
// dws is the DingTalk side-effect boundary: as a named tool or shelled out in a command.
|
|
1029
|
+
const commandTexts = toolUses.flatMap((item) => {
|
|
1030
|
+
const message = {
|
|
1031
|
+
tool: optionalString(item.tool), input: smokeToolInput(item),
|
|
1032
|
+
};
|
|
1033
|
+
return active.map((vocabulary) => vocabulary.commandText(message)).filter(Boolean);
|
|
1034
|
+
});
|
|
1035
|
+
const forbiddenTools = toolNames.filter((name) => forbiddenSmokeTool(name));
|
|
1036
|
+
const forbiddenCommands = commandTexts.some((text) => commandInvokesDws(text));
|
|
1037
|
+
const knownTools = new Set(active.flatMap((item) => [...item.tools]));
|
|
1038
|
+
const unrecognizedTools = toolNames.filter((name) => !knownTools.has(name) && !evidenceTools.has(name) && !forbiddenSmokeTool(name));
|
|
1039
|
+
// Response evidence, most stable source first: the Issue's own comments (platform
|
|
1040
|
+
// readback — no tool vocabulary involved), then trace texts, writes and shell heredocs.
|
|
1041
|
+
const matchesResponse = (value) => smokeResponseMatches(value, marker, requiredSkills);
|
|
1042
|
+
let commentContents = [];
|
|
1043
|
+
try {
|
|
1044
|
+
commentContents = jsonArray(runProvider(command, scopedArgs(plan, [
|
|
1045
|
+
'issue', 'comment', 'list', issueId, '--recent', '20', '--output', 'json',
|
|
1046
|
+
]), 'smoke.issue.comments', 'read', root, env, timeoutMs, calls).stdout, 'smoke comments').map((item) => optionalString(item.content)).filter(Boolean);
|
|
1047
|
+
}
|
|
1048
|
+
catch {
|
|
1049
|
+
commentContents = []; // 旧版 provider CLI 可能没有 comment list;回退 trace 内取证
|
|
1050
|
+
}
|
|
860
1051
|
const texts = messages.filter((item) => optionalString(item.type) === 'text')
|
|
861
1052
|
.map((item) => optionalString(item.content)).filter(Boolean);
|
|
862
1053
|
const finalText = texts.at(-1) || '';
|
|
863
|
-
const
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1054
|
+
const responseWrites = [];
|
|
1055
|
+
const responseShells = [];
|
|
1056
|
+
for (const item of toolUses) {
|
|
1057
|
+
const message = {
|
|
1058
|
+
tool: optionalString(item.tool), input: smokeToolInput(item),
|
|
1059
|
+
};
|
|
1060
|
+
for (const vocabulary of active) {
|
|
1061
|
+
const write = vocabulary.writeEvidence(message);
|
|
1062
|
+
if (write && safeSmokeResponsePath(write.path))
|
|
1063
|
+
responseWrites.push(write.content);
|
|
1064
|
+
const text = vocabulary.commandText(message);
|
|
1065
|
+
if (text && safeCombinedSmokeResponse(text, issueId, marker, requiredSkills)) {
|
|
1066
|
+
responseShells.push(JSON.stringify({
|
|
1067
|
+
schema: 'dta-multica-load-smoke@1', marker, loaded: requiredSkills,
|
|
1068
|
+
}));
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
const responseEvidence = [...commentContents, ...texts, ...responseWrites, ...responseShells]
|
|
872
1073
|
.find(matchesResponse) || '';
|
|
873
1074
|
const responsePassed = Boolean(responseEvidence);
|
|
874
|
-
const toolTracePassed =
|
|
1075
|
+
const toolTracePassed = skillEvidencePassed && !forbiddenTools.length && !forbiddenCommands;
|
|
875
1076
|
const failures = [];
|
|
1077
|
+
const failureDetails = [];
|
|
876
1078
|
if (taskStatus !== 'completed')
|
|
877
1079
|
failures.push(`smoke.task.${taskStatus || 'unknown'}`);
|
|
878
|
-
if (!
|
|
879
|
-
failures.push('smoke.skill-
|
|
880
|
-
|
|
1080
|
+
if (!skillEvidencePassed) {
|
|
1081
|
+
failures.push('smoke.skill-load-evidence');
|
|
1082
|
+
const attemptedMissing = requiredSkills.filter((name) => attempted.has(name) && !evidence.has(name));
|
|
1083
|
+
failureDetails.push({
|
|
1084
|
+
code: 'smoke.skill-load-evidence',
|
|
1085
|
+
expected: requiredSkills,
|
|
1086
|
+
actual: loadedSkills,
|
|
1087
|
+
hint: attemptedMissing.length
|
|
1088
|
+
? `有装载尝试但无成功证据(tool_result 缺 **Skill loaded**): ${attemptedMissing.join(', ')};` +
|
|
1089
|
+
'尝试不算装载成功'
|
|
1090
|
+
: unrecognizedTools.length
|
|
1091
|
+
? `轨迹里有词汇表不认识的工具名: ${unrecognizedTools.join(', ')};` +
|
|
1092
|
+
'若属新 runtime 词汇,需要在 multica-runtime-vocabulary 注册'
|
|
1093
|
+
: '轨迹中没有这些 Skill 的装载证据(tool_result 的 **Skill loaded** 或 Skill 工具调用)',
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
for (const tool of forbiddenTools)
|
|
1097
|
+
failures.push(`smoke.forbidden-tool:${tool}`);
|
|
1098
|
+
if (forbiddenCommands)
|
|
1099
|
+
failures.push('smoke.forbidden-command:dws');
|
|
1100
|
+
if (!responsePassed) {
|
|
881
1101
|
failures.push('smoke.response');
|
|
1102
|
+
failureDetails.push({
|
|
1103
|
+
code: 'smoke.response',
|
|
1104
|
+
actual: `comments=${commentContents.length} texts=${texts.length} writes=${responseWrites.length}`,
|
|
1105
|
+
hint: '未在 Issue comment、回复文本或落盘文件中找到精确的 dta-multica-load-smoke@1 JSON',
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
// 分类冷沙箱竞态:任务终态且缺装载证据,轨迹里出现 skill not found 类措辞 → 可重试,
|
|
1109
|
+
// 与「配置/模板真错了」区分开(B7 的两个误导方向都来自不区分这两态)。
|
|
1110
|
+
let classification = '';
|
|
1111
|
+
if (!skillEvidencePassed) {
|
|
1112
|
+
const searchable = messages.map((item) => optionalString(item.content)).join('\n');
|
|
1113
|
+
if (/skill[^\n]{0,80}?not\s+found|not\s+found[^\n]{0,60}?skill|skill\s+view\s+failed/i.test(searchable)) {
|
|
1114
|
+
classification = 'skills-not-visible';
|
|
1115
|
+
failures.unshift('smoke.skills-not-visible');
|
|
1116
|
+
failureDetails.unshift({
|
|
1117
|
+
code: 'smoke.skills-not-visible',
|
|
1118
|
+
hint: '推送的 Skill 尚未物化进沙箱(冷启动竞态,通常数分钟内自愈);' +
|
|
1119
|
+
'用 deploy --status --execute --yes 重试,或部署时用 --wait 放宽预算',
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
882
1123
|
return {
|
|
883
1124
|
status: failures.length ? 'failed' : 'passed', marker, markerHash: sha256(marker),
|
|
884
1125
|
issueId, taskId, taskStatus, taskDiagnostic: remoteTaskDiagnostic(run), requiredSkills, loadedSkills,
|
|
885
1126
|
toolTracePassed, responsePassed, responseHash: sha256(responseEvidence || finalText), failures,
|
|
1127
|
+
runtimeProvider: provider, vocabularyIds, toolNames, unrecognizedTools, classification,
|
|
1128
|
+
failureDetails,
|
|
886
1129
|
};
|
|
887
1130
|
}
|
|
888
1131
|
function smokeToolInput(item) {
|
|
@@ -900,46 +1143,6 @@ function smokeResponseMatches(value, marker, requiredSkills) {
|
|
|
900
1143
|
return false;
|
|
901
1144
|
}
|
|
902
1145
|
}
|
|
903
|
-
function allowedSmokeToolUse(item, issueId, marker, requiredSkills) {
|
|
904
|
-
const tool = optionalString(item.tool);
|
|
905
|
-
if (tool === 'skill')
|
|
906
|
-
return true;
|
|
907
|
-
const input = smokeToolInput(item);
|
|
908
|
-
if (tool === 'write') {
|
|
909
|
-
return safeSmokeResponsePath(optionalString(input.filePath)) &&
|
|
910
|
-
smokeResponseMatches(optionalString(input.content), marker, requiredSkills);
|
|
911
|
-
}
|
|
912
|
-
if (tool !== 'bash')
|
|
913
|
-
return false;
|
|
914
|
-
const command = optionalString(input.command);
|
|
915
|
-
if (new Set([
|
|
916
|
-
`multica issue get ${issueId} --output json`,
|
|
917
|
-
`multica issue metadata list ${issueId} --output json`,
|
|
918
|
-
`multica issue comment list ${issueId} --recent 10 --output json`,
|
|
919
|
-
`multica issue status ${issueId} in_progress`,
|
|
920
|
-
`multica issue status ${issueId} in_review`,
|
|
921
|
-
]).has(command))
|
|
922
|
-
return true;
|
|
923
|
-
const commentPrefix = `multica issue comment add ${issueId} --content-file `;
|
|
924
|
-
if (command.startsWith(commentPrefix)) {
|
|
925
|
-
return safeSmokeResponsePath(command.slice(commentPrefix.length));
|
|
926
|
-
}
|
|
927
|
-
const cleanupFirstPrefix = 'rm ';
|
|
928
|
-
const cleanupFirstSuffix = ` && multica issue status ${issueId} in_review`;
|
|
929
|
-
if (command.startsWith(cleanupFirstPrefix) && command.endsWith(cleanupFirstSuffix)) {
|
|
930
|
-
return safeSmokeCleanup(command.slice(cleanupFirstPrefix.length, command.length - cleanupFirstSuffix.length));
|
|
931
|
-
}
|
|
932
|
-
const statusFirstPrefix = `multica issue status ${issueId} in_review && rm `;
|
|
933
|
-
if (command.startsWith(statusFirstPrefix)) {
|
|
934
|
-
return safeSmokeCleanup(command.slice(statusFirstPrefix.length));
|
|
935
|
-
}
|
|
936
|
-
if (command.startsWith(cleanupFirstPrefix)) {
|
|
937
|
-
return safeSmokeCleanup(command.slice(cleanupFirstPrefix.length));
|
|
938
|
-
}
|
|
939
|
-
if (safeCombinedSmokeResponse(command, issueId, marker, requiredSkills))
|
|
940
|
-
return true;
|
|
941
|
-
return false;
|
|
942
|
-
}
|
|
943
1146
|
function safeCombinedSmokeResponse(command, issueId, marker, requiredSkills) {
|
|
944
1147
|
const response = JSON.stringify({ schema: 'dta-multica-load-smoke@1', marker, loaded: requiredSkills });
|
|
945
1148
|
return command === [
|
|
@@ -963,9 +1166,6 @@ function safeSmokeResponsePath(value) {
|
|
|
963
1166
|
return value.split('/').every((part) => !part ||
|
|
964
1167
|
(part !== '.' && part !== '..' && /^[A-Za-z0-9._-]+$/.test(part)));
|
|
965
1168
|
}
|
|
966
|
-
function safeSmokeCleanup(value) {
|
|
967
|
-
return safeSmokeResponsePath(value.startsWith('-f ') ? value.slice(3) : value);
|
|
968
|
-
}
|
|
969
1169
|
function rollbackConfirmed(root, plan, command, env, timeoutMs, calls, rollback, temp) {
|
|
970
1170
|
const report = emptyRollback(true);
|
|
971
1171
|
const attempt = (id, argv) => {
|
|
@@ -1079,7 +1279,7 @@ function verifyRollback(root, plan, command, env, timeoutMs, calls, rollback, re
|
|
|
1079
1279
|
});
|
|
1080
1280
|
}
|
|
1081
1281
|
}
|
|
1082
|
-
function failureReceipt(root, workspace, source, plan, preflight, operation, calls, rollback, status, failure) {
|
|
1282
|
+
function failureReceipt(root, workspace, source, plan, preflight, operation, calls, rollback, status, failure, failureDetail) {
|
|
1083
1283
|
const receiptId = `multica_receipt_${digest(stableStringify({
|
|
1084
1284
|
operationId: operation.operationId, status, failure,
|
|
1085
1285
|
})).slice(0, 24)}`;
|
|
@@ -1117,6 +1317,7 @@ function failureReceipt(root, workspace, source, plan, preflight, operation, cal
|
|
|
1117
1317
|
smoke: emptySmoke('not-run', source.skills.map((item) => item.name)),
|
|
1118
1318
|
rollback,
|
|
1119
1319
|
failures: [failure, ...rollback.failures],
|
|
1320
|
+
failureDetails: failureDetail ? [failureDetail] : [],
|
|
1120
1321
|
evidencePath: receiptRelativePath(workspace.name, receiptId),
|
|
1121
1322
|
remoteRead: true,
|
|
1122
1323
|
remoteWrite: calls.some((call) => call.kind === 'write' || call.kind === 'rollback'),
|
|
@@ -1136,6 +1337,7 @@ export function statusMulticaDeployment(start, name, options = {}) {
|
|
|
1136
1337
|
workspace: name, provider: 'multica', available: false, operationId: '', receiptId: '',
|
|
1137
1338
|
status: 'missing', passed: false, providerReady: false,
|
|
1138
1339
|
operationIntegrity: false, receiptIntegrity: false, reconciled: false,
|
|
1340
|
+
receiptFrozen: false, smokeStale: false, nextAction: '',
|
|
1139
1341
|
failures: ['deployment.missing'], receipt: null,
|
|
1140
1342
|
remoteRead: false, remoteWrite: false, triggerWrite: false,
|
|
1141
1343
|
sideEffect: false, dingtalkSideEffect: false,
|
|
@@ -1143,6 +1345,7 @@ export function statusMulticaDeployment(start, name, options = {}) {
|
|
|
1143
1345
|
let { operation, receipt } = loaded;
|
|
1144
1346
|
let reconciled = false;
|
|
1145
1347
|
let remoteRead = false;
|
|
1348
|
+
let remoteWrite = false;
|
|
1146
1349
|
if (options.execute) {
|
|
1147
1350
|
if (!options.yes)
|
|
1148
1351
|
throw new Error('deploy --status 远端 reconcile 必须同时传 --execute --yes');
|
|
@@ -1151,12 +1354,41 @@ export function statusMulticaDeployment(start, name, options = {}) {
|
|
|
1151
1354
|
const timeoutMs = positiveTimeout(options.timeoutMs, 30_000, 300_000);
|
|
1152
1355
|
if (receipt.status === 'verifying' && receipt.smoke.issueId) {
|
|
1153
1356
|
remoteRead = true;
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1357
|
+
let smokeReadback = readSmokeFromReceipt(info.root, receipt, command, env, timeoutMs);
|
|
1358
|
+
let smoke = smokeReadback.smoke;
|
|
1359
|
+
let calls = smokeReadback.calls;
|
|
1360
|
+
// 冷沙箱竞态的收口入口:终态失败但分类为 skills-not-visible 时,补一次 mention
|
|
1361
|
+
// 触发新 task(唯一的写;如实上报 remoteWrite/sideEffect)。新 run 是异步创建的:
|
|
1362
|
+
// 以 taskId 变化为「新 run 出现」的判据再判定,等不到就按旧判定收场(仍 verifying),
|
|
1363
|
+
// 不再重复 mention。
|
|
1364
|
+
if (smoke.status === 'failed' && smoke.classification === 'skills-not-visible' &&
|
|
1365
|
+
receipt.agent.id) {
|
|
1366
|
+
const planLike = {
|
|
1367
|
+
profile: receipt.profile, expected: { workspaceId: operation.workspaceId },
|
|
1368
|
+
};
|
|
1369
|
+
const staleTaskId = smoke.taskId;
|
|
1370
|
+
runProvider(command, scopedArgs(planLike, [
|
|
1371
|
+
'issue', 'comment', 'add', receipt.smoke.issueId,
|
|
1372
|
+
'--content', `[@DTA load smoke](mention://agent/${receipt.agent.id}) Skills were not yet visible in the sandbox; re-run the deployment verification exactly as specified in this Issue description.`,
|
|
1373
|
+
'--output', 'json',
|
|
1374
|
+
]), 'smoke.issue.retry', 'write', info.root, env, timeoutMs, calls);
|
|
1375
|
+
remoteWrite = true;
|
|
1376
|
+
const deadline = Date.now() + timeoutMs;
|
|
1377
|
+
do {
|
|
1378
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2_000);
|
|
1379
|
+
smokeReadback = readSmokeFromReceipt(info.root, { ...receipt, calls }, command, env, timeoutMs);
|
|
1380
|
+
smoke = smokeReadback.smoke;
|
|
1381
|
+
calls = smokeReadback.calls;
|
|
1382
|
+
} while ((smoke.status === 'pending' ||
|
|
1383
|
+
(Boolean(staleTaskId) && smoke.taskId === staleTaskId)) && Date.now() < deadline);
|
|
1384
|
+
}
|
|
1385
|
+
const nextStatus = smoke.status === 'passed' ? 'ready'
|
|
1386
|
+
: smoke.status === 'pending' ? 'verifying'
|
|
1387
|
+
: smoke.classification === 'skills-not-visible' ? 'verifying' : 'failed';
|
|
1388
|
+
receipt = { ...receipt, calls, smoke, status: nextStatus,
|
|
1158
1389
|
passed: smoke.status === 'passed', providerReady: smoke.status === 'passed',
|
|
1159
|
-
failures: [...smoke.failures]
|
|
1390
|
+
failures: [...smoke.failures],
|
|
1391
|
+
failureDetails: [...smoke.failureDetails] };
|
|
1160
1392
|
reconciled = true;
|
|
1161
1393
|
persistReceiptAndState(info.root, showDevelopmentWorkspace(info.root, name), operation, receipt);
|
|
1162
1394
|
}
|
|
@@ -1190,6 +1422,7 @@ export function statusMulticaDeployment(start, name, options = {}) {
|
|
|
1190
1422
|
}
|
|
1191
1423
|
operation = loadOperationAndReceipt(info.root, name, operation.operationId)?.operation || operation;
|
|
1192
1424
|
}
|
|
1425
|
+
const receiptFrozen = !options.execute;
|
|
1193
1426
|
return {
|
|
1194
1427
|
$schema: 'dingtalk-agent/multica-deployment-status@1',
|
|
1195
1428
|
workspace: name,
|
|
@@ -1203,12 +1436,17 @@ export function statusMulticaDeployment(start, name, options = {}) {
|
|
|
1203
1436
|
operationIntegrity: true,
|
|
1204
1437
|
receiptIntegrity: true,
|
|
1205
1438
|
reconciled,
|
|
1439
|
+
receiptFrozen,
|
|
1440
|
+
smokeStale: receiptFrozen && receipt.status === 'verifying',
|
|
1441
|
+
nextAction: ['verifying', 'reconciling'].includes(receipt.status)
|
|
1442
|
+
? `dta deploy --workspace ${name} --status --operation-id ${operation.operationId} --execute --yes`
|
|
1443
|
+
: '',
|
|
1206
1444
|
failures: [...receipt.failures],
|
|
1207
1445
|
receipt,
|
|
1208
1446
|
remoteRead,
|
|
1209
|
-
remoteWrite
|
|
1447
|
+
remoteWrite,
|
|
1210
1448
|
triggerWrite: false,
|
|
1211
|
-
sideEffect: remoteRead ? 'multica-read' : false,
|
|
1449
|
+
sideEffect: remoteWrite ? 'multica-read-write' : remoteRead ? 'multica-read' : false,
|
|
1212
1450
|
dingtalkSideEffect: false,
|
|
1213
1451
|
};
|
|
1214
1452
|
}
|
|
@@ -1334,7 +1572,7 @@ function readSmokeFromReceipt(root, receipt, command, env, timeoutMs) {
|
|
|
1334
1572
|
failures: ['smoke.marker-not-recoverable'],
|
|
1335
1573
|
} };
|
|
1336
1574
|
}
|
|
1337
|
-
const smoke = readSmoke(root, plan, markerValue, receipt.smoke.issueId, receipt.agent.id, receipt.smoke.requiredSkills, command, env, timeoutMs, calls);
|
|
1575
|
+
const smoke = readSmoke(root, plan, markerValue, receipt.smoke.issueId, receipt.agent.id, receipt.smoke.requiredSkills, receipt.smoke.runtimeProvider || '', command, env, timeoutMs, calls);
|
|
1338
1576
|
return { smoke, calls };
|
|
1339
1577
|
}
|
|
1340
1578
|
function persistReceiptAndState(root, workspace, operation, receipt) {
|
|
@@ -1504,7 +1742,7 @@ function runProvider(command, argv, id, kind, cwd, env, timeoutMs, calls) {
|
|
|
1504
1742
|
throw new DeploymentFailure('budget.rollback-exhausted');
|
|
1505
1743
|
}
|
|
1506
1744
|
const run = spawnSync(command, argv, {
|
|
1507
|
-
cwd, env, encoding: 'utf8', timeout: timeoutMs,
|
|
1745
|
+
cwd, env: sanitizeProviderEnv(env).env, encoding: 'utf8', timeout: timeoutMs,
|
|
1508
1746
|
maxBuffer: 32 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'],
|
|
1509
1747
|
});
|
|
1510
1748
|
const stdout = String(run.stdout || '');
|
|
@@ -1516,8 +1754,10 @@ function runProvider(command, argv, id, kind, cwd, env, timeoutMs, calls) {
|
|
|
1516
1754
|
id, kind, argv: redactArgv(argv), exitCode,
|
|
1517
1755
|
stdoutHash: sha256(stdout), stderrHash: sha256(stderr), passed, ambiguous,
|
|
1518
1756
|
});
|
|
1519
|
-
if (!passed)
|
|
1757
|
+
if (!passed) {
|
|
1758
|
+
emitProviderDiagnostic(id, exitCode, stderr || stdout);
|
|
1520
1759
|
throw new DeploymentFailure(`command.${id}`, ambiguous);
|
|
1760
|
+
}
|
|
1521
1761
|
return { stdout, stderr };
|
|
1522
1762
|
}
|
|
1523
1763
|
function scopedArgs(plan, args) {
|
|
@@ -1638,6 +1878,8 @@ function emptySmoke(status, requiredSkills) {
|
|
|
1638
1878
|
status, marker: '', markerHash: '', issueId: '', taskId: '', taskStatus: '', taskDiagnostic: '',
|
|
1639
1879
|
requiredSkills: [...requiredSkills].sort(), loadedSkills: [],
|
|
1640
1880
|
toolTracePassed: false, responsePassed: false, responseHash: '', failures: [],
|
|
1881
|
+
runtimeProvider: '', vocabularyIds: [], toolNames: [], unrecognizedTools: [],
|
|
1882
|
+
classification: '', failureDetails: [],
|
|
1641
1883
|
};
|
|
1642
1884
|
}
|
|
1643
1885
|
function emptyRollback(attempted) {
|