@sema-agent/client-core 0.72.14 → 0.73.1
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 +45 -0
- package/README.md +2 -1
- package/dist/controlRouter.d.ts +4 -1
- package/dist/controlRouter.js +5 -0
- package/dist/engineAgentPanelStore.js +10 -2
- package/dist/fleet/fleetLedger.d.ts +0 -4
- package/dist/fleet/fleetLedger.js +12 -4
- package/dist/fleet/fleetProjection.d.ts +20 -8
- package/dist/fleet/fleetProjection.js +20 -8
- package/dist/fleetAgentPanelProjection.js +26 -11
- package/dist/hitl/planReviewWire.d.ts +17 -5
- package/dist/hitl/planReviewWire.js +83 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/wireRefusalCopy.d.ts +44 -0
- package/dist/wireRefusalCopy.js +110 -0
- package/dist/workflowClient.js +23 -6
- package/dist/workflowMonitor.d.ts +2 -1
- package/docs/INTEGRATION-CLIENTS.md +90 -7
- package/package.json +1 -1
|
@@ -346,6 +346,7 @@ opts) {
|
|
|
346
346
|
// 首见只损失「reopened」一词 —— 首见文案零历史断言,诚实方向安全。
|
|
347
347
|
notePlanReviewAnswered(questionId);
|
|
348
348
|
// fire-and-forget:resume 同步驱动到终态可能分钟级,不能挂住 overlay;结果经 queue 通知回来
|
|
349
|
+
hostLog('debug', `planReviewWire: arm responder delivering ${decided} for task ${taskId} (path=arm)`);
|
|
349
350
|
void decidePlanReview(taskId, decided, modeAfter);
|
|
350
351
|
return { ok: true };
|
|
351
352
|
});
|
|
@@ -364,11 +365,45 @@ opts) {
|
|
|
364
365
|
return false;
|
|
365
366
|
}
|
|
366
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* outcome 尾句的**唯一铸点**(给模型的下一步指令)。🔴 只有 `took_effect` 才许说「已批准并汇报做了什么 / 已拒绝」;
|
|
370
|
+
* 其余三态一律不许出现把决断当成已生效的措辞 —— 那会让模型向用户汇报一件没发生的事。
|
|
371
|
+
*/
|
|
372
|
+
export function planReviewOutcomeTail(taskId, decision, effect) {
|
|
373
|
+
switch (effect) {
|
|
374
|
+
case 'took_effect':
|
|
375
|
+
return decision === 'approve'
|
|
376
|
+
? `Use TaskOutput("${taskId}") if you need the resumed task's full output, then give the user a brief report of what was done.`
|
|
377
|
+
: 'Briefly acknowledge to the user that the plan was rejected and continue planning.';
|
|
378
|
+
case 'advanced':
|
|
379
|
+
return `Tell the user the ${decision} went through and that another approval is now waiting for them; do not report the task as finished.`;
|
|
380
|
+
case 'still_parked':
|
|
381
|
+
return `Do not tell the user the plan was ${decision === 'approve' ? 'approved' : 'rejected'}: the engine still holds the plan at the same review gate — it is still waiting for a decision.`;
|
|
382
|
+
case 'not_sent':
|
|
383
|
+
return `Nothing was sent to the engine. Do not tell the user the plan was ${decision === 'approve' ? 'approved' : 'rejected'}; the plan is still waiting for a decision.`;
|
|
384
|
+
case 'not_applied':
|
|
385
|
+
return `The ${decision} did not take effect. Do not tell the user the plan was ${decision === 'approve' ? 'approved' : 'rejected'}; check the task's current state with TaskOutput("${taskId}") before saying anything about the plan.`;
|
|
386
|
+
case 'unconfirmed':
|
|
387
|
+
return `Do not state the result as fact: tell the user the ${decision} was submitted but its effect could not be confirmed; TaskOutput("${taskId}") shows where the task stands.`;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
367
390
|
/** POST the decision; on settle enqueue an isMeta turn so the model reports the resume outcome.
|
|
368
391
|
* SDK 宪法迁移批:SDK 0.0.52 assistant.planReview verb(同 wire `POST /v1/assistant/tasks/:id/
|
|
369
392
|
* plan_review` 同 body {decision})——raw fetch 退役。错误模型:非 2xx 抛 APIError(message =
|
|
370
393
|
* server body.error 原文)→ 同款「HTTP <status> <error>」outcome 文案;网络失败走原「could not
|
|
371
394
|
* reach the engine」臂。 */
|
|
395
|
+
/**
|
|
396
|
+
* 投递口在飞集(0.73.1):同 taskId 的第二次 decide 在第一次 **POST 落地之前**到达 = 重复投递(下游定谳的 0.9 s 双 approve 形)。
|
|
397
|
+
* 🔴 窗只到 POST 落地,**不盖**其后的状态回拉(最长 15 s)与 outcome 投递:引擎收下 approve 后可以立刻续跑又停一张新门,
|
|
398
|
+
* 那张新门的作答是合法的第二次决断,回拉期间到达不许被吞(异源对抗复审 [high])。
|
|
399
|
+
* 被闩住的那次:错误级留痕、不发第二条 meta prompt(第二条会让模型对用户汇报两次)。
|
|
400
|
+
*/
|
|
401
|
+
const decideInFlight = new Map();
|
|
402
|
+
let decideDispatchSeq = 0;
|
|
403
|
+
/** 测试钩:清投递口闩。 */
|
|
404
|
+
export function __resetPlanReviewDecisionLatchForTests() {
|
|
405
|
+
decideInFlight.clear();
|
|
406
|
+
}
|
|
372
407
|
export async function decidePlanReview(taskId, decision,
|
|
373
408
|
/**
|
|
374
409
|
* 0.72.13 CC-46(engine ≥7.86.0;契约 §4c):批准之后用哪一档。缺席 ⇒ 体逐字节同旧 `{decision}`。只配 approve、闭集两词 ——
|
|
@@ -380,17 +415,47 @@ permissionModeAfter) {
|
|
|
380
415
|
// (armPlanReviewApproval 的 responder 早就 `return {ok:true}` 过了),但决断本身连一次网络
|
|
381
416
|
// 请求都没发出去,而模型/用户没有任何回程信号。两条早退现在都汇进同一条 outcome 管道(下方
|
|
382
417
|
// 唯一的 enqueue 调用点),不再是两条独立的静默 return。
|
|
418
|
+
// 0.73.1 投递口闩:在飞中的第二次 decide 直接拒(错误级留痕;不发第二条 meta prompt —— 那会让模型汇报两次)。
|
|
419
|
+
if (decideInFlight.has(taskId)) {
|
|
420
|
+
hostLog('error', `planReviewWire: decidePlanReview(${decision}) for task ${taskId} REFUSED — a decision for this task is already in flight (duplicate delivery latched)`);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
// 所有权 token:只有持闩的那次调用能放闩(异源对抗复审:旧调用回拉完成后的 finally 曾无条件 delete,把新调用刚取的闩删掉)。
|
|
424
|
+
const token = {};
|
|
425
|
+
decideInFlight.set(taskId, token);
|
|
426
|
+
const releaseLatch = () => {
|
|
427
|
+
if (decideInFlight.get(taskId) === token)
|
|
428
|
+
decideInFlight.delete(taskId);
|
|
429
|
+
};
|
|
430
|
+
const dispatchNo = ++decideDispatchSeq;
|
|
431
|
+
hostLog('debug', `planReviewWire: decidePlanReview dispatch #${dispatchNo} task=${taskId} decision=${decision} mode=${permissionModeAfter ?? '-'}`);
|
|
432
|
+
try {
|
|
433
|
+
await decidePlanReviewInner(taskId, decision, permissionModeAfter, dispatchNo, releaseLatch);
|
|
434
|
+
}
|
|
435
|
+
finally {
|
|
436
|
+
releaseLatch(); // 没走到 POST 的早退路径(本地拒 / 无连接)也要放闩;幂等,持闩者才真放
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
async function decidePlanReviewInner(taskId, decision, permissionModeAfter, dispatchNo,
|
|
440
|
+
/** **最终**一次 POST 落地(成功或失败;含 default 去键重发)那一刻调:闩窗到此为止。幂等。 */
|
|
441
|
+
releaseLatch) {
|
|
383
442
|
let outcome;
|
|
443
|
+
// 0.73.1:决断**有没有生效** —— 尾句按它选,不按 `decision` 无条件追加。此前 approve 恒追加「取续跑结果并汇报做了什么」、
|
|
444
|
+
// reject 恒追加「告知用户计划已拒绝」,于是「could NOT be sent」「did NOT take effect」的正文后面跟着一句让模型把没生效的
|
|
445
|
+
// 决断当成已生效去汇报的指令(同一段 meta prompt 自相矛盾)。缺省 = 未确认(任何一条路径忘了归类,都落在最保守的那一句上)。
|
|
446
|
+
let effect = 'unconfirmed';
|
|
384
447
|
// 🔴 design/285 批 3 显式裁定:本处与 `armPlanReviewApproval` 同一笔豁免(理由逐字见那边)——
|
|
385
448
|
// 决断是从 arm 立的那张卡的 responder 回调进来的,槽键必须与立卡时同源,单换这一跳即两头不一致。
|
|
386
449
|
const cfg = engineWireTarget();
|
|
387
450
|
const badMode = permissionModeAfter !== undefined && (decision !== 'approve' || !isPlanReviewModeAfter(permissionModeAfter));
|
|
388
451
|
if (badMode) {
|
|
389
452
|
hostLog('error', `planReviewWire: ${decision} NOT sent — permissionModeAfter is only valid with approve and must be "default" or "acceptEdits"`);
|
|
453
|
+
effect = 'not_sent';
|
|
390
454
|
outcome = `The plan_review ${decision} could NOT be sent: the post-approval permission mode given with it is not valid for this decision (it only applies to an approval, and must be "default" or "acceptEdits"). Tell the user plainly that the decision did not go through; the plan is still waiting.`;
|
|
391
455
|
}
|
|
392
456
|
else if (!cfg) {
|
|
393
457
|
hostLog('error', `planReviewWire: ${decision} NOT sent — engineWireTarget() unavailable`);
|
|
458
|
+
effect = 'not_sent';
|
|
394
459
|
outcome = `The plan_review ${decision} could NOT be sent: no engine connection is configured on this host. Tell the user plainly that the decision did not go through.`;
|
|
395
460
|
}
|
|
396
461
|
else {
|
|
@@ -402,6 +467,7 @@ permissionModeAfter) {
|
|
|
402
467
|
});
|
|
403
468
|
if (!client) {
|
|
404
469
|
hostLog('error', `planReviewWire: ${decision} NOT sent — makeEngineWireClient() returned null`);
|
|
470
|
+
effect = 'not_sent';
|
|
405
471
|
outcome = `The plan_review ${decision} could NOT be sent: the engine client could not be constructed. Tell the user plainly that the decision did not go through.`;
|
|
406
472
|
}
|
|
407
473
|
else {
|
|
@@ -421,13 +487,17 @@ permissionModeAfter) {
|
|
|
421
487
|
const conflict = e1;
|
|
422
488
|
if (mode === 'default' && conflict?.status === 400 && conflict?.errorCode === 'request.field_conflict') {
|
|
423
489
|
hostLog('debug', 'planReviewWire: permissionModeAfter "default" refused as not applicable — re-sending the approval without the key (same meaning)');
|
|
490
|
+
// 去键重发仍属**原决断**(400 明确没改状态)⇒ 闩不放,直到重发落地(异源对抗复审 [medium])。
|
|
424
491
|
body = await client.assistant.planReview(taskId, { decision });
|
|
425
492
|
}
|
|
426
493
|
else {
|
|
427
494
|
throw e1;
|
|
428
495
|
}
|
|
429
496
|
}
|
|
430
|
-
|
|
497
|
+
finally {
|
|
498
|
+
releaseLatch(); // 最终一次 POST 落地(成功 / 400 / 重发成败)⇒ 闩窗到此为止;其后的回拉与 outcome 不在窗内
|
|
499
|
+
}
|
|
500
|
+
hostLog('debug', `planReviewWire: ${decision} → ok (dispatch #${dispatchNo}) ${JSON.stringify(body).slice(0, 200)}`);
|
|
431
501
|
// [2315]/[2316](#109,2026-08-02):decide 的 **2xx 不当终态** —— test 黑盒实测 reject 9/9
|
|
432
502
|
// 返回 200 而会话仍锁在同一 gate(core RB-471:重开兜底对 reject 腿恒真误触发)。这里回拉
|
|
433
503
|
// 一次任务状态,按真形分三路措辞;根因归 core/server,本腿是「对外动作回读验证」在产品面的
|
|
@@ -450,6 +520,7 @@ permissionModeAfter) {
|
|
|
450
520
|
// 回拉失败时回落 decide 200 体自带的 status(次级来源;两者都缺=unverified,措辞如实降级)。
|
|
451
521
|
const effective = postStatus ?? (typeof body?.status === 'string' ? body.status : undefined);
|
|
452
522
|
if (effective === 'needs_review') {
|
|
523
|
+
effect = 'still_parked';
|
|
453
524
|
// 决定没生效,仍锁原 gate。不渲「已处理」;真出路只有 approve 或 cancel(下一次提交撞 409
|
|
454
525
|
// 时 activeRunSelfHeal 会把审批卡重开——这里不自动重弹卡,避免「刚拒绝又弹卡」的突袭感,
|
|
455
526
|
// 决定权经卡的重开路径还给用户)。
|
|
@@ -459,16 +530,19 @@ permissionModeAfter) {
|
|
|
459
530
|
`Tell the user plainly that the ${decision} did not go through; the reliable exits today are approving the plan or cancelling the task.`;
|
|
460
531
|
}
|
|
461
532
|
else if (effective === 'suspended') {
|
|
533
|
+
effect = 'advanced';
|
|
462
534
|
// 合法推进到新 gate(test [2315] ②形):不是「完成」,如实说下一张审批卡会跟上。
|
|
463
535
|
outcome =
|
|
464
536
|
`The plan was ${decision === 'approve' ? 'approved' : 'rejected'} and the task advanced to a NEW approval gate ` +
|
|
465
537
|
`(post-decide status: suspended) — the next approval card will surface it; this is not a completion yet.`;
|
|
466
538
|
}
|
|
467
539
|
else if (effective !== undefined) {
|
|
540
|
+
effect = 'took_effect';
|
|
468
541
|
outcome = `The plan was ${decision === 'approve' ? 'approved and the parked task resumed to completion' : 'rejected (plan discarded)'} — final status: ${effective} (post-decide re-checked: task left the review gate).`;
|
|
469
542
|
}
|
|
470
543
|
else {
|
|
471
|
-
|
|
544
|
+
// 复审 R1:正文也不许断言成功 —— 200 只证明引擎受理了请求,效果是什么本包此刻不知道。
|
|
545
|
+
outcome = `The plan_review ${decision} was accepted by the engine (HTTP 200) but its effect is unconfirmed — final status: unknown (post-decide verification unavailable). Do not treat this as ${decision === 'approve' ? 'an approval that resumed the task' : 'a discarded plan'}.`;
|
|
472
546
|
}
|
|
473
547
|
}
|
|
474
548
|
catch (e) {
|
|
@@ -479,6 +553,7 @@ permissionModeAfter) {
|
|
|
479
553
|
// CC-46:这条任务不是只读起步的(模型自选 plan 模式那种形)⇒ 引擎拒收「批准后自动接受编辑」。**不**静默降档重发:
|
|
480
554
|
// 用户选的是「不再逐次征询」,悄悄换成「逐次征询」= 替他改了决定。引擎一个字节的状态都没动,计划仍在等。
|
|
481
555
|
hostLog('debug', 'planReviewWire: approve + acceptEdits refused (request.field_conflict) — NOT re-sent with a different mode');
|
|
556
|
+
effect = 'still_parked'; // 契约:被这道门拒掉的请求一个字节的状态都不动 ⇒ 计划仍停在同一张门上,这一形能确定
|
|
482
557
|
outcome =
|
|
483
558
|
'The plan approval was NOT applied: the engine refused "auto-accept edits" for this task (it was not started read-only in plan mode, so that option does not apply). ' +
|
|
484
559
|
'Nothing changed engine-side — the plan is still waiting for review. Tell the user plainly, and that approving with "manually approve edits" will go through.';
|
|
@@ -486,6 +561,8 @@ permissionModeAfter) {
|
|
|
486
561
|
else if (typeof status === 'number') {
|
|
487
562
|
const msg = e instanceof Error ? e.message : String(e);
|
|
488
563
|
hostLog('debug', `planReviewWire: ${decision} → ${status} ${msg.slice(0, 200)}`);
|
|
564
|
+
// 4xx = 引擎明确拒收(状态没动);5xx / 其它 = 不知道引擎那头做到了哪一步 ⇒ 未确认
|
|
565
|
+
effect = status >= 400 && status < 500 ? 'not_applied' : 'unconfirmed';
|
|
489
566
|
outcome = `The plan_review decision failed: HTTP ${status} ${msg}`.trim();
|
|
490
567
|
}
|
|
491
568
|
else {
|
|
@@ -499,7 +576,7 @@ permissionModeAfter) {
|
|
|
499
576
|
// `enqueueMetaPrompt`(notifications.ts 头注:「模型永远不知道 plan 被批准/驳回后引擎跑出了
|
|
500
577
|
// 什么结果」),这是与 HTTP 失败同等重量的静默丢失,必须留痕。
|
|
501
578
|
try {
|
|
502
|
-
const delivered = enqueuePlanReviewOutcome(`<plan-review-outcome>\n${outcome}${
|
|
579
|
+
const delivered = enqueuePlanReviewOutcome(`<plan-review-outcome>\n${outcome}\n${planReviewOutcomeTail(taskId, decision, effect)}\n</plan-review-outcome>`);
|
|
503
580
|
if (!delivered) {
|
|
504
581
|
hostLog('error', `planReviewWire: outcome enqueue MISSED (queue port not installed or lacks enqueueMetaPrompt) for task ${taskId} — model will not learn the ${decision} result`);
|
|
505
582
|
}
|
|
@@ -578,6 +655,7 @@ export function reopenPlanReviewCard(taskId, opts) {
|
|
|
578
655
|
const firstSight = !wasGateArmedFor(sessionKey, armedKey);
|
|
579
656
|
const mintFresh = opts?.mintFreshQuestionId !== false;
|
|
580
657
|
if (!mintFresh && hasLocalQuestionResponder(canonicalId)) {
|
|
658
|
+
hostLog('debug', `planReviewWire: reopen for task ${taskId} reusing the canonical responder (path=reopen-canonical)`);
|
|
581
659
|
// 重呈短路(与 arm 臂的重放短路同形):同一个 canonical 身份、同一份题面、同一个 responder。
|
|
582
660
|
// 卡还开着的宿主只是收到一帧同形重绘,卡已被 dismiss 的宿主拿回重开路径。
|
|
583
661
|
// 0.29.0 发包扫描门:本臂沿用 arm responder ⇒ opts.deliverDecision 不生效(JSDoc 成文例外),
|
|
@@ -618,10 +696,12 @@ export function reopenPlanReviewCard(taskId, opts) {
|
|
|
618
696
|
}
|
|
619
697
|
publishQuestionFrameFor(sessionKey, { type: 'question_complete', questionId: canonicalId });
|
|
620
698
|
const questionId = `${canonicalId}${REOPEN_ID_TAIL}${reopenIdSuffix()}`;
|
|
699
|
+
hostLog('debug', `planReviewWire: reopen for task ${taskId} minting a fresh card ${questionId} (path=reopen-fresh)`);
|
|
621
700
|
const deliver = opts?.deliverDecision ??
|
|
622
701
|
((tid, decision) => {
|
|
623
702
|
// fire-and-forget:resume 同步驱动到终态可能分钟级,不能挂住 overlay(arm 臂同姿势);
|
|
624
703
|
// 失败处置/结果回植由 decidePlanReview 自带的 outcome 管道承担。
|
|
704
|
+
hostLog('debug', `planReviewWire: reopen responder delivering ${decision} for task ${tid} (path=reopen-default)`);
|
|
625
705
|
void decidePlanReview(tid, decision);
|
|
626
706
|
});
|
|
627
707
|
const activeKey = `${sessionKey}\u0000${canonicalId}`;
|
package/dist/index.d.ts
CHANGED
|
@@ -240,6 +240,7 @@ export * from './engineWireTarget.js';
|
|
|
240
240
|
export * from './principalWire.js';
|
|
241
241
|
export * from './wireErrorTriage.js';
|
|
242
242
|
export * from './resumeRefusalCopy.js';
|
|
243
|
+
export * from './wireRefusalCopy.js';
|
|
243
244
|
export * from './decideReceipt.js';
|
|
244
245
|
export * from './sessionMap.js';
|
|
245
246
|
export * from './detachWire.js';
|
package/dist/index.js
CHANGED
|
@@ -387,6 +387,8 @@ export * from './wireErrorTriage.js';
|
|
|
387
387
|
// 本口答「该对人说什么」(文案),`preflight_rejected` 的窗与可等性**转调**前者不复制判定。
|
|
388
388
|
// 壳侧对位 = cli `src/sema/resumeRefusalCopy.ts`(1.0.101 起改薄成适配层)。
|
|
389
389
|
export * from './resumeRefusalCopy.js';
|
|
390
|
+
// · wireRefusalCopy(0.73.1):cancel 的 409 按机器码分家(两种相反的处置)+ 提交面 429 用量窗的读口与人话面。
|
|
391
|
+
export * from './wireRefusalCopy.js';
|
|
390
392
|
// · decideReceipt:B-070 / L-200(0.65.x)—— `/decide` **答了什么**的三端单一读面。
|
|
391
393
|
// 🔴 两件事靠它,而两件都不是「门解决了没有」(200 只是**投递受理**,sdk README §9.0.0):
|
|
392
394
|
// ① `handoffTaskId` —— workflow 车道把续跑交给**宿主新铸**的 run(「poll 它,不要盯那张卡」);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wireRefusalCopy — 两条 wire 拒绝的三端共用读口与人话面(0.73.1)。
|
|
3
|
+
*
|
|
4
|
+
* ① `POST /v1/runs/:id/cancel` 的 **409 是两个码、两种相反的处置**:
|
|
5
|
+
* · `conflict.approval_settled` —— park 臂:取消打在一条停驻的 run 上,而那扇审批门被并发的决断 / 回收抢先结算了。
|
|
6
|
+
* 处置 = **重读决议结果**(别人已经替这条 run 做了决定),不是盲目重试。
|
|
7
|
+
* · `conflict.run_not_running` —— running 臂:取消旗反复挂不上(行在 running 与 park 之间来回跳,有界重分类用尽)。
|
|
8
|
+
* 处置 = **直接重试**。
|
|
9
|
+
* 只看 409 状态码会把两者合成一句,其中一半人被指错路。码缺席 / 认不出 ⇒ `unrecognized`,不猜。
|
|
10
|
+
*
|
|
11
|
+
* ② 提交面 **429 `usage.window_exhausted`**:部署级用量窗耗尽,**可等待**的拒绝(窗滑动后原样重发即可)。
|
|
12
|
+
* `retryAfterSec` 在场就说等多久,缺席就不编数。它与续跑面的 `resume.usage_window_exhausted` 是两个码、两句话 ——
|
|
13
|
+
* 后者说的是一条已停驻的 run 续跑时撞窗(决定没被消费、token 仍可赎),套到提交面上每一个字都不对。
|
|
14
|
+
*
|
|
15
|
+
* 读口只认 `errorCode`(退役的 `code` 槽不做兼容),永不抛;措辞单源在这里,端只拼接不另写。
|
|
16
|
+
*/
|
|
17
|
+
import { USAGE_WINDOW_EXHAUSTED } from './engineErrorCodes.js';
|
|
18
|
+
/** cancel 的 409 · park 臂:审批门已被并发结算。 */
|
|
19
|
+
export declare const CONFLICT_APPROVAL_SETTLED = "conflict.approval_settled";
|
|
20
|
+
/** cancel 的 409 · running 臂:取消旗反复挂不上。 */
|
|
21
|
+
export declare const CONFLICT_RUN_NOT_RUNNING = "conflict.run_not_running";
|
|
22
|
+
export type CancelConflictKind = 'approval_settled' | 'run_not_running' | 'unrecognized';
|
|
23
|
+
/** `code` 在场 = 引擎给的原码(`unrecognized` 时也原样带出,便于留痕);缺席 = 这条 409 没带机器码。 */
|
|
24
|
+
export interface CancelConflictDetail {
|
|
25
|
+
kind: CancelConflictKind;
|
|
26
|
+
code?: string;
|
|
27
|
+
}
|
|
28
|
+
/** cancel 抛出的错误 → 409 三形;不是 409(状态不是 409 且不是 sdk 的 `ConflictError`)/ 非对象 ⇒ `null`。 */
|
|
29
|
+
export declare function cancelConflictFromError(err: unknown): CancelConflictDetail | null;
|
|
30
|
+
/** 三句人话(唯一措辞真源)。🔴 两种处置不许说反:settled 句不提重试取消,not_running 句不说已被决定。 */
|
|
31
|
+
export declare function cancelConflictContent(detail: Pick<CancelConflictDetail, 'kind'>): string;
|
|
32
|
+
/** 提交面用量窗耗尽的读数。`retryAfterSec` 缺席 = 引擎没说要等多久(绝不编一个数)。 */
|
|
33
|
+
export interface UsageWindowExhaustedDetail {
|
|
34
|
+
code: typeof USAGE_WINDOW_EXHAUSTED;
|
|
35
|
+
retryAfterSec?: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 只认 **HTTP 429 载体**上的 `errorCode === "usage.window_exhausted"`;同码落在终态 run 记录上的形(跑到 turn 边界才被停下)**不在本读面**
|
|
39
|
+
* (那一形工作可能已经发生,处置相反 —— 先看结果再决定怎么续)。
|
|
40
|
+
* 等待量两个来源:回体 `retryAfterSec`(秒)优先;缺席时读 `retryAfterMs`(sdk 从 `Retry-After` 头喂入)上取整换算。
|
|
41
|
+
*/
|
|
42
|
+
export declare function usageWindowExhaustedFromError(err: unknown): UsageWindowExhaustedDetail | null;
|
|
43
|
+
/** 一句人话(唯一措辞真源):有数带数,无数不编数;说清这是部署的用量窗、不是请求写错了。 */
|
|
44
|
+
export declare function usageWindowExhaustedContent(detail: UsageWindowExhaustedDetail): string;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wireRefusalCopy — 两条 wire 拒绝的三端共用读口与人话面(0.73.1)。
|
|
3
|
+
*
|
|
4
|
+
* ① `POST /v1/runs/:id/cancel` 的 **409 是两个码、两种相反的处置**:
|
|
5
|
+
* · `conflict.approval_settled` —— park 臂:取消打在一条停驻的 run 上,而那扇审批门被并发的决断 / 回收抢先结算了。
|
|
6
|
+
* 处置 = **重读决议结果**(别人已经替这条 run 做了决定),不是盲目重试。
|
|
7
|
+
* · `conflict.run_not_running` —— running 臂:取消旗反复挂不上(行在 running 与 park 之间来回跳,有界重分类用尽)。
|
|
8
|
+
* 处置 = **直接重试**。
|
|
9
|
+
* 只看 409 状态码会把两者合成一句,其中一半人被指错路。码缺席 / 认不出 ⇒ `unrecognized`,不猜。
|
|
10
|
+
*
|
|
11
|
+
* ② 提交面 **429 `usage.window_exhausted`**:部署级用量窗耗尽,**可等待**的拒绝(窗滑动后原样重发即可)。
|
|
12
|
+
* `retryAfterSec` 在场就说等多久,缺席就不编数。它与续跑面的 `resume.usage_window_exhausted` 是两个码、两句话 ——
|
|
13
|
+
* 后者说的是一条已停驻的 run 续跑时撞窗(决定没被消费、token 仍可赎),套到提交面上每一个字都不对。
|
|
14
|
+
*
|
|
15
|
+
* 读口只认 `errorCode`(退役的 `code` 槽不做兼容),永不抛;措辞单源在这里,端只拼接不另写。
|
|
16
|
+
*/
|
|
17
|
+
import { USAGE_WINDOW_EXHAUSTED } from './engineErrorCodes.js';
|
|
18
|
+
/** cancel 的 409 · park 臂:审批门已被并发结算。 */
|
|
19
|
+
export const CONFLICT_APPROVAL_SETTLED = 'conflict.approval_settled';
|
|
20
|
+
/** cancel 的 409 · running 臂:取消旗反复挂不上。 */
|
|
21
|
+
export const CONFLICT_RUN_NOT_RUNNING = 'conflict.run_not_running';
|
|
22
|
+
function readStringField(o, key) {
|
|
23
|
+
const v = o[key];
|
|
24
|
+
return typeof v === 'string' && v !== '' ? v : undefined;
|
|
25
|
+
}
|
|
26
|
+
/** cancel 抛出的错误 → 409 三形;不是 409(状态不是 409 且不是 sdk 的 `ConflictError`)/ 非对象 ⇒ `null`。 */
|
|
27
|
+
export function cancelConflictFromError(err) {
|
|
28
|
+
try {
|
|
29
|
+
if (err === null || typeof err !== 'object')
|
|
30
|
+
return null;
|
|
31
|
+
const o = err;
|
|
32
|
+
const rawStatus = o.status ?? o.statusCode;
|
|
33
|
+
const is409 = rawStatus === 409 || (rawStatus === undefined && o.name === 'ConflictError');
|
|
34
|
+
if (!is409)
|
|
35
|
+
return null;
|
|
36
|
+
const code = readStringField(o, 'errorCode');
|
|
37
|
+
if (code === CONFLICT_APPROVAL_SETTLED)
|
|
38
|
+
return { kind: 'approval_settled', code };
|
|
39
|
+
if (code === CONFLICT_RUN_NOT_RUNNING)
|
|
40
|
+
return { kind: 'run_not_running', code };
|
|
41
|
+
return { kind: 'unrecognized', ...(code !== undefined ? { code } : {}) };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** 三句人话(唯一措辞真源)。🔴 两种处置不许说反:settled 句不提重试取消,not_running 句不说已被决定。 */
|
|
48
|
+
export function cancelConflictContent(detail) {
|
|
49
|
+
switch (detail.kind) {
|
|
50
|
+
case 'approval_settled':
|
|
51
|
+
return ('The cancel did not apply: the approval this task was waiting on has already been decided or has expired elsewhere · ' +
|
|
52
|
+
'Check what that decision did to the task before doing anything else');
|
|
53
|
+
case 'run_not_running':
|
|
54
|
+
return ('The cancel could not be set: the task kept switching between running and waiting while the engine tried · ' +
|
|
55
|
+
'Nothing was changed — send the cancel again');
|
|
56
|
+
case 'unrecognized':
|
|
57
|
+
return ('The engine refused this cancel with a conflict it did not explain · ' +
|
|
58
|
+
'The task may have just been decided elsewhere, or may have changed state mid-cancel — check its current state, then cancel again if it is still active');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function nonNegativeFinite(v) {
|
|
62
|
+
return typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 只认 **HTTP 429 载体**上的 `errorCode === "usage.window_exhausted"`;同码落在终态 run 记录上的形(跑到 turn 边界才被停下)**不在本读面**
|
|
66
|
+
* (那一形工作可能已经发生,处置相反 —— 先看结果再决定怎么续)。
|
|
67
|
+
* 等待量两个来源:回体 `retryAfterSec`(秒)优先;缺席时读 `retryAfterMs`(sdk 从 `Retry-After` 头喂入)上取整换算。
|
|
68
|
+
*/
|
|
69
|
+
export function usageWindowExhaustedFromError(err) {
|
|
70
|
+
try {
|
|
71
|
+
if (err === null || typeof err !== 'object')
|
|
72
|
+
return null;
|
|
73
|
+
const o = err;
|
|
74
|
+
if (o.errorCode !== USAGE_WINDOW_EXHAUSTED)
|
|
75
|
+
return null;
|
|
76
|
+
// 🔴 只认 **HTTP 429 载体**(状态 429,或 sdk 的 UsageWindowExhaustedError / RateLimitedError 名):同一个码也会落在终态 run 记录上
|
|
77
|
+
// (跑到 turn 边界才被用量窗停下,同样带 retryAfterMs)—— 那不是「提交未受理」,套这句话会把已经发生的工作说成没做。
|
|
78
|
+
const rawStatus = o.status ?? o.statusCode;
|
|
79
|
+
const is429 = rawStatus === 429 || (rawStatus === undefined && (o.name === 'UsageWindowExhaustedError' || o.name === 'RateLimitedError'));
|
|
80
|
+
if (!is429)
|
|
81
|
+
return null;
|
|
82
|
+
const sec = nonNegativeFinite(o.retryAfterSec)
|
|
83
|
+
? Math.ceil(o.retryAfterSec)
|
|
84
|
+
: nonNegativeFinite(o.retryAfterMs)
|
|
85
|
+
? Math.ceil(o.retryAfterMs / 1000)
|
|
86
|
+
: undefined;
|
|
87
|
+
return { code: USAGE_WINDOW_EXHAUSTED, ...(sec !== undefined ? { retryAfterSec: sec } : {}) };
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function humanWait(sec) {
|
|
94
|
+
if (sec < 60)
|
|
95
|
+
return `${sec} s`;
|
|
96
|
+
const m = Math.floor(sec / 60);
|
|
97
|
+
const s = sec % 60;
|
|
98
|
+
if (m < 60)
|
|
99
|
+
return s === 0 ? `${m} min` : `${m} min ${s} s`;
|
|
100
|
+
const h = Math.floor(m / 60);
|
|
101
|
+
const mm = m % 60;
|
|
102
|
+
return mm === 0 ? `${h} h` : `${h} h ${mm} min`;
|
|
103
|
+
}
|
|
104
|
+
/** 一句人话(唯一措辞真源):有数带数,无数不编数;说清这是部署的用量窗、不是请求写错了。 */
|
|
105
|
+
export function usageWindowExhaustedContent(detail) {
|
|
106
|
+
const head = "The deployment's usage window is exhausted, so the engine did not accept this request · Nothing is wrong with the request itself";
|
|
107
|
+
return detail.retryAfterSec !== undefined
|
|
108
|
+
? `${head} · Send it again in about ${humanWait(detail.retryAfterSec)}`
|
|
109
|
+
: `${head} · The engine did not say how long the window needs — send it again a little later`;
|
|
110
|
+
}
|
package/dist/workflowClient.js
CHANGED
|
@@ -385,11 +385,28 @@ export function projectWorkflowRun(run) {
|
|
|
385
385
|
const parks = readWorkflowParks(run);
|
|
386
386
|
const agents = (run.agents ?? []).map((r, i) => projectAgent(r, i));
|
|
387
387
|
const phases = projectPhases(run, agents);
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
388
|
+
// 0.73.0:总用量三态 —— 有键 ⇒ 一个**完整**的总数(含真 0);缺键 ⇒ 没有完整总数。判据(每个分量都先验非负整数):
|
|
389
|
+
// ① 引擎聚合 `stats.tokens` 在场且合法:无 nested 载体 ⇒ 就是它;nested 载体在场 ⇒ 其 tokens 也得合法才相加,否则缺席
|
|
390
|
+
// (残缺的聚合不按 0 补齐 —— 那会发布一个偏小的「总数」);
|
|
391
|
+
// ② `stats.tokens` 缺席但 nested 载体在场 ⇒ 聚合残缺 ⇒ 缺席(不退到腿上求和:腿只覆盖本 run,会把嵌套的那部分漏掉);
|
|
392
|
+
// ③ 两者都没有 ⇒ 退到腿上求和,但只在**每条腿都报过合法用量**且至少一条腿时成立;有腿未报 / 非法 / 零条腿 ⇒ 缺席。
|
|
393
|
+
const validTokens = (v) => typeof v === 'number' && Number.isInteger(v) && v >= 0 ? v : undefined;
|
|
394
|
+
const ownTokens = validTokens(run.stats?.tokens);
|
|
395
|
+
const nestedCarrier = run.stats?.nested;
|
|
396
|
+
const hasNestedCarrier = nestedCarrier !== undefined && nestedCarrier !== null;
|
|
397
|
+
const nestedTokens = hasNestedCarrier ? validTokens(nestedCarrier.tokens) : undefined;
|
|
398
|
+
const legTokens = agents.map(a => validTokens(a.tokens));
|
|
399
|
+
const totalTokens = ownTokens !== undefined
|
|
400
|
+
? !hasNestedCarrier
|
|
401
|
+
? ownTokens
|
|
402
|
+
: nestedTokens !== undefined
|
|
403
|
+
? ownTokens + nestedTokens
|
|
404
|
+
: undefined
|
|
405
|
+
: hasNestedCarrier
|
|
406
|
+
? undefined
|
|
407
|
+
: legTokens.length > 0 && legTokens.every(t => t !== undefined)
|
|
408
|
+
? legTokens.reduce((s, t) => s + t, 0)
|
|
409
|
+
: undefined;
|
|
393
410
|
const state = {
|
|
394
411
|
workflowRunId: run.id,
|
|
395
412
|
// 0.72.12 CC-51(异源对抗复审 R2 [medium]):run 状态词认不出 / 缺席时先看腿上的**活跃证据** —— 有腿在跑 / 排队 ⇒ running
|
|
@@ -404,7 +421,7 @@ export function projectWorkflowRun(run) {
|
|
|
404
421
|
// a script we do not have. scriptPath is likewise absent until the service projects it.
|
|
405
422
|
script: '',
|
|
406
423
|
agentCount: agents.length,
|
|
407
|
-
totalTokens,
|
|
424
|
+
...(totalTokens !== undefined ? { totalTokens } : {}),
|
|
408
425
|
phases,
|
|
409
426
|
};
|
|
410
427
|
if (run.name)
|
|
@@ -90,7 +90,8 @@ export interface WorkflowRunState {
|
|
|
90
90
|
status: 'running' | 'done' | 'failed' | 'stopped' | 'paused' | 'parked' | 'unknown';
|
|
91
91
|
cloud?: boolean;
|
|
92
92
|
agentCount: number;
|
|
93
|
-
|
|
93
|
+
/** 🔴 0.73.0(BREAKING):可选。有键 ⇒ 总用量(含真 0);缺键 ⇒ 没有权威总数且有腿未报(或零条腿)—— 渲「—」,不渲 0。 */
|
|
94
|
+
totalTokens?: number;
|
|
94
95
|
/** phases as authored; the component derives the active/clamped windows itself. */
|
|
95
96
|
phases: WorkflowPhase[];
|
|
96
97
|
/**
|