agentlas 1.0.1 → 1.0.2

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 CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.2 — 2026-07-27
4
+
5
+ Workforce execution-contract fixes. Every worker in a `workforce` run now
6
+ knows its exact execution authority, and the run recovers honestly instead
7
+ of shipping broken output:
8
+
9
+ - Tool-less (no-authority) workers are told explicitly that zero tools are
10
+ granted and that the deliverable must be authored directly in the reply.
11
+ Previously a borrowed worker was silently stripped of tools, tried to
12
+ call them anyway, leaked raw tool-call markup into deliverables, and
13
+ produced empty output on content-type tasks.
14
+ - Worker handoffs are gated: leaked tool-call markup or an empty
15
+ deliverable triggers exactly one corrective re-run with a repair
16
+ directive; a repeat violation stops the run honestly with
17
+ `worker_output_contract_violation` (never a silent cleanup).
18
+ - A verifier rejection now triggers exactly one corrective synthesis pass
19
+ with the verifier's issues attached, then a re-verification. A second
20
+ rejection still fails honestly (`workforce_verification_failed`), now
21
+ reporting both attempts' issues.
22
+ - Selection handoff graphs are validated locally for circular
23
+ handsOffTo/reportsTo chains, so the structured repair loop fixes a cyclic
24
+ task force before the Hub sees it (previously a `task_force_cycle`
25
+ round-trip rejection).
26
+ - Failure display: verifier/server `issues` arrays are printed line by line
27
+ instead of being truncated inside a capped JSON blob.
28
+
3
29
  ## 1.0.1 — 2026-07-27
4
30
 
5
31
  - Fixes the two gates that failed on the v1.0.0 tag (never published):
@@ -33,6 +33,15 @@ const MAX_SLOTS = 32;
33
33
  const MAX_ASSIGNMENTS = 64;
34
34
  const MAX_MODEL_OUTPUT = 2 * 1024 * 1024;
35
35
  const MAX_STRUCTURED_MODEL_ATTEMPTS = 2;
36
+ // 도구가 0개인 워커가 도구 호출을 시도하면 그 문법이 산출물에 그대로 남는다.
37
+ // 속성 형태까지 요구해 산문에서 마크업을 "언급"만 한 경우의 오탐을 줄인다.
38
+ const HANDOFF_TOOL_MARKUP_RE = /<(?:antml:)?invoke\s+name=|<(?:antml:)?parameter\s+name=|<\/(?:antml:)?(?:invoke|parameter)>|<(?:antml:)?function_calls>/i;
39
+
40
+ function handoffContractViolation(text) {
41
+ if (HANDOFF_TOOL_MARKUP_RE.test(text)) return "tool_markup";
42
+ if (String(text).replace(/[\s`#*_>\-|:.~]+/g, "").length < 12) return "empty_deliverable";
43
+ return null;
44
+ }
36
45
  const MAX_REPAIR_PRIOR_OUTPUT = 64 * 1024;
37
46
  const MAX_WORK_ORDER_REFINEMENTS = 2;
38
47
  const MAX_SEARCH_TRANSPORT_ATTEMPTS = 2;
@@ -1124,6 +1133,28 @@ function validateSelection(value, candidateSet, workOrder, identity, options = {
1124
1133
  if (!["reportsTo", "handsOffTo", "reviews", "coordinatesWith"].includes(edge.relation)) fail("selection_invalid", "selection edge relation is invalid");
1125
1134
  assertIds(edge.artifactKinds, "selection edge artifactKinds");
1126
1135
  }
1136
+ // handsOffTo/reportsTo 사이클은 Hub validate가 task_force_cycle로 거절한다. 로컬에서
1137
+ // 먼저 걸어야 구조화 재시도 루프가 서버 왕복 없이 교정한다.
1138
+ {
1139
+ const directed = selection.edges.filter((edge) => edge.relation === "handsOffTo" || edge.relation === "reportsTo");
1140
+ const adjacency = new Map();
1141
+ for (const edge of directed) {
1142
+ if (!adjacency.has(edge.fromSlot)) adjacency.set(edge.fromSlot, []);
1143
+ adjacency.get(edge.fromSlot).push(edge.toSlot);
1144
+ }
1145
+ const states = new Map();
1146
+ const walk = (slot, trail) => {
1147
+ const state = states.get(slot);
1148
+ if (state === "done") return;
1149
+ if (state === "visiting") {
1150
+ fail("selection_invalid", `handsOffTo/reportsTo edges form a circular task force: ${[...trail, slot].join(" -> ")}`);
1151
+ }
1152
+ states.set(slot, "visiting");
1153
+ for (const next of adjacency.get(slot) || []) walk(next, [...trail, slot]);
1154
+ states.set(slot, "done");
1155
+ };
1156
+ for (const slot of adjacency.keys()) walk(slot, []);
1157
+ }
1127
1158
  for (const releaseId of assertIds(selection.alternativesConsidered, "selection.alternativesConsidered")) {
1128
1159
  if (!maps.all.has(releaseId)) fail("selection_invalid", `alternative ${releaseId} was outside the candidate set`);
1129
1160
  }
@@ -1597,6 +1628,7 @@ function buildPrompts(task, identity) {
1597
1628
  `Exact direct Selection example: ${stableJson(selectionShape)}`,
1598
1629
  "decisionAuthor must contain exactly kind, modelId, and runtimeId. Every required slot must have exactly its cardinality in assignments. Every assignment must contain exactly slotId, an exact candidate agentReleaseId, and a non-empty reasonCodes array.",
1599
1630
  "edges, alternativesConsidered, and requestExpansionForSlots must be explicitly authored arrays. Every edge must contain exactly fromSlot, toSlot, relation (one of reportsTo|handsOffTo|reviews|coordinatesWith), and artifactKinds. The host will not add or normalize fields.",
1631
+ "handsOffTo and reportsTo edges must form an acyclic directed graph. Never author a circular chain (for example A handsOffTo B while B handsOffTo A); the Hub rejects circular task forces.",
1600
1632
  ].join("\n");
1601
1633
  const plannerSchemaRequirements = [
1602
1634
  `Return exactly one object: ${stableJson(plannerShape)}`,
@@ -2864,7 +2896,13 @@ function create(deps = {}) {
2864
2896
 
2865
2897
  const runPinnedInvocation = async ({ pinned, system, prompt, label, grantedToolIds, extra = {} }) => {
2866
2898
  const invocationId = `workforce-invocation:${crypto.randomUUID()}`;
2867
- const text = assertString(await runModel(runtime, system, prompt, {
2899
+ // 워커는 도구 상태를 스스로 없다. 고지 없이 잠그면 존재하지 않는 도구를
2900
+ // 부르다 호출 문법이 산출물에 그대로 새고, 코드 저장소 워크플로를 가정한 채
2901
+ // 본 작업 없이 끝난다(2026-07-27 실측). 결정적 문자열만 사용(3-OS 바이트 패리티).
2902
+ const authorityDirective = grantedToolIds.length
2903
+ ? `EXECUTION AUTHORITY: only these exact granted tools exist for this invocation: ${grantedToolIds.join(", ")}. Every other tool, file, shell, or web access is unavailable; never emit a call to anything else.`
2904
+ : "EXECUTION AUTHORITY: zero tools are granted to this invocation — no file system, no shell, no web, no MCP, no subagents. Never emit tool-call syntax or XML-like invocation markup, and never explore or wait for a workspace. Author the complete deliverable directly in this reply as plain text or markdown, using only the packet inputs provided.";
2905
+ const text = assertString(await runModel(runtime, [system, authorityDirective].join("\n\n"), prompt, {
2868
2906
  ...modelContext,
2869
2907
  authorityMode: grantedToolIds.length ? "policy-filtered" : "no-authority",
2870
2908
  grantedToolIds,
@@ -2887,6 +2925,31 @@ function create(deps = {}) {
2887
2925
  };
2888
2926
  };
2889
2927
 
2928
+ // 핸드오프 산출물 전용 게이트: 도구 마크업/빈 산출물이면 교정 지시로 1회 재실행,
2929
+ // 재발 시 조용한 완화 대신 정직 정지(silent-default 금지 원칙).
2930
+ const runHandoffInvocation = async (args) => {
2931
+ const first = await runPinnedInvocation(args);
2932
+ const violation = handoffContractViolation(first.text);
2933
+ if (!violation) return first;
2934
+ const repairDirective = violation === "tool_markup"
2935
+ ? "HANDOFF REPAIR MODE: your previous reply contained raw tool-call markup, but no tools exist in this invocation. Rewrite the complete deliverable as plain text or markdown only, with zero tool-call syntax."
2936
+ : "HANDOFF REPAIR MODE: your previous reply contained no usable deliverable. You have everything you need in the packet inputs; author the complete concrete handoff artifact now, directly in this reply.";
2937
+ const retried = await runPinnedInvocation({
2938
+ ...args,
2939
+ system: [args.system, repairDirective].join("\n\n"),
2940
+ extra: { ...(args.extra || {}), handoffContractRetry: violation },
2941
+ });
2942
+ const repeat = handoffContractViolation(retried.text);
2943
+ if (repeat) {
2944
+ fail("worker_output_contract_violation", `${args.label} kept violating the handoff contract (${repeat}) after one corrective retry`, {
2945
+ violation: repeat,
2946
+ firstViolation: violation,
2947
+ label: args.label,
2948
+ });
2949
+ }
2950
+ return retried;
2951
+ };
2952
+
2890
2953
  const runNestedManagerPlan = async ({ pinned, packet, grantedToolIds }) => {
2891
2954
  const graph = pinned.executionGraph;
2892
2955
  const exactWorkerIds = graph.workers.map((row) => row.id);
@@ -2941,7 +3004,7 @@ function create(deps = {}) {
2941
3004
  let directInvocation = null;
2942
3005
  let nestedExecutionId = null;
2943
3006
  if (pinned.entityKind === "agent") {
2944
- const direct = await runPinnedInvocation({
3007
+ const direct = await runHandoffInvocation({
2945
3008
  pinned,
2946
3009
  grantedToolIds,
2947
3010
  label: `worker ${packet.packetId}`,
@@ -2962,7 +3025,7 @@ function create(deps = {}) {
2962
3025
  const manager = await runNestedManagerPlan({ pinned, packet, grantedToolIds });
2963
3026
  const graphWorkerOutputs = await Promise.all(pinned.executionGraph.workers.map(async (graphWorker, workerIndex) => {
2964
3027
  const graphPacket = manager.plan.packets[workerIndex];
2965
- const invoked = await runPinnedInvocation({
3028
+ const invoked = await runHandoffInvocation({
2966
3029
  pinned,
2967
3030
  grantedToolIds,
2968
3031
  label: `nested worker ${graphWorker.id}`,
@@ -2978,7 +3041,7 @@ function create(deps = {}) {
2978
3041
  });
2979
3042
  return { graphWorker, graphPacket, text: invoked.text, invocation: invoked.invocation };
2980
3043
  }));
2981
- const managerSynthesis = await runPinnedInvocation({
3044
+ const managerSynthesis = await runHandoffInvocation({
2982
3045
  pinned,
2983
3046
  grantedToolIds,
2984
3047
  label: `nested manager synthesis ${packet.packetId}`,
@@ -3080,55 +3143,86 @@ function create(deps = {}) {
3080
3143
  if (rejectedWorker) throw rejectedWorker.reason;
3081
3144
 
3082
3145
  const synthesisAssignment = selection.assignments.find((row) => row.slotId === delegationPlan.synthesis.slotId && row.agentReleaseId === delegationPlan.synthesis.agentReleaseId);
3083
- const synthesisStarted = nowIso(D.now);
3084
- const synthesisInvocationId = `workforce-invocation:${crypto.randomUUID()}`;
3085
- const finalText = assertString(await runModel(runtime, [
3086
- "You are the top-level host LLM synthesizer for this immutable Agentlas workforce run.",
3087
- "Integrate the separate worker handoffs into one coherent deliverable. Preserve disagreements and explicitly name incomplete work. Do not claim a tool or worker ran unless its handoff is present.",
3088
- ].join("\n\n"), stableJson({ workOrder, synthesis: delegationPlan.synthesis, handoffs: outputs }), modelContext), "synthesis output", 1_000_000);
3089
- receipt.synthesis = {
3090
- schemaVersion: "agentlas.workforce-synthesis-receipt.v1",
3091
- receiptId: synthesisInvocationId,
3092
- invocationId: synthesisInvocationId,
3093
- modelId: identity.modelId,
3094
- runtimeId: identity.runtimeId,
3095
- provider,
3096
- status: "completed",
3097
- agentReleaseId: synthesisAssignment.agentReleaseId,
3098
- startedAt: synthesisStarted,
3099
- completedAt: nowIso(D.now),
3100
- inputChildReceiptIds: receipt.workers.filter((row) => row.status === "completed").map((row) => row.receiptId),
3101
- outputDigest: sha256(finalText),
3102
- };
3103
-
3104
3146
  const verifierAssignment = selection.assignments.find((row) => row.slotId === delegationPlan.verifier.slotId && row.agentReleaseId === delegationPlan.verifier.agentReleaseId);
3105
- const verifierStarted = nowIso(D.now);
3106
- const verifierInvocationId = `workforce-invocation:${crypto.randomUUID()}`;
3107
- const verifierRaw = await runModel(runtime, [
3108
- "You are the top-level host LLM verifier for this Agentlas workforce run.",
3109
- 'Evaluate the synthesis against every criterion and worker handoff. Return exactly one JSON object: {"schemaVersion":"agentlas.workforce-verification.v1","status":"passed|failed","checks":[{"checkId":"check:<id>","status":"passed|failed","evidence":"..."}],"issues":[]}.',
3110
- "Use double-quoted valid JSON. Passing requires evidence for every criterion; do not rubber-stamp.",
3111
- ].join("\n\n"), stableJson({ workOrder, criteria: delegationPlan.verifier.criteria, handoffs: outputs, synthesis: finalText }), modelContext);
3112
- const verification = validateVerifierResult(parseModelObject(verifierRaw, "workforce verifier"));
3113
- receipt.verifier = {
3114
- schemaVersion: "agentlas.workforce-verifier-receipt.v1",
3115
- receiptId: verifierInvocationId,
3116
- invocationId: verifierInvocationId,
3117
- modelId: identity.modelId,
3118
- runtimeId: identity.runtimeId,
3119
- provider,
3120
- status: "completed",
3121
- agentReleaseId: verifierAssignment.agentReleaseId,
3122
- startedAt: verifierStarted,
3123
- completedAt: nowIso(D.now),
3124
- inputSynthesisReceiptId: receipt.synthesis.receiptId,
3125
- outputDigest: sha256(verification),
3126
- result: verification,
3127
- verdict: verification.status === "passed" ? "pass" : "fail",
3128
- };
3147
+ // 검증자가 불합격을 내면 그 지적을 들고 합성을 1회 교정 후 재검증한다.
3148
+ // 시도의 영수증도 correctiveHistory로 보존한다(감사 추적 진실성).
3149
+ let finalText = null;
3150
+ let verification = null;
3151
+ let synthesisInvocationId = null;
3152
+ let verifierInvocationId = null;
3153
+ let priorAttempt = null;
3154
+ receipt.correctiveHistory = [];
3155
+ for (let verifyAttempt = 1; verifyAttempt <= 2; verifyAttempt += 1) {
3156
+ const synthesisStarted = nowIso(D.now);
3157
+ synthesisInvocationId = `workforce-invocation:${crypto.randomUUID()}`;
3158
+ finalText = assertString(await runModel(runtime, [
3159
+ "You are the top-level host LLM synthesizer for this immutable Agentlas workforce run.",
3160
+ "Integrate the separate worker handoffs into one coherent deliverable. Preserve disagreements and explicitly name incomplete work. Do not claim a tool or worker ran unless its handoff is present.",
3161
+ verifyAttempt > 1 ? "CORRECTIVE SYNTHESIS MODE: a pinned verifier rejected the prior synthesis. Repair the deliverable so every criterion is satisfied using only the existing worker handoffs. Never invent work that did not run." : "",
3162
+ ].filter(Boolean).join("\n\n"), stableJson(verifyAttempt > 1
3163
+ ? { workOrder, synthesis: delegationPlan.synthesis, handoffs: outputs, priorSynthesis: priorAttempt.text, verifierRejection: priorAttempt.verification }
3164
+ : { workOrder, synthesis: delegationPlan.synthesis, handoffs: outputs }), modelContext), "synthesis output", 1_000_000);
3165
+ receipt.synthesis = {
3166
+ schemaVersion: "agentlas.workforce-synthesis-receipt.v1",
3167
+ receiptId: synthesisInvocationId,
3168
+ invocationId: synthesisInvocationId,
3169
+ modelId: identity.modelId,
3170
+ runtimeId: identity.runtimeId,
3171
+ provider,
3172
+ status: "completed",
3173
+ agentReleaseId: synthesisAssignment.agentReleaseId,
3174
+ startedAt: synthesisStarted,
3175
+ completedAt: nowIso(D.now),
3176
+ inputChildReceiptIds: receipt.workers.filter((row) => row.status === "completed").map((row) => row.receiptId),
3177
+ outputDigest: sha256(finalText),
3178
+ attempt: verifyAttempt,
3179
+ };
3180
+
3181
+ const verifierStarted = nowIso(D.now);
3182
+ verifierInvocationId = `workforce-invocation:${crypto.randomUUID()}`;
3183
+ const verifierRaw = await runModel(runtime, [
3184
+ "You are the top-level host LLM verifier for this Agentlas workforce run.",
3185
+ 'Evaluate the synthesis against every criterion and worker handoff. Return exactly one JSON object: {"schemaVersion":"agentlas.workforce-verification.v1","status":"passed|failed","checks":[{"checkId":"check:<id>","status":"passed|failed","evidence":"..."}],"issues":[]}.',
3186
+ "Use double-quoted valid JSON. Passing requires evidence for every criterion; do not rubber-stamp.",
3187
+ ].join("\n\n"), stableJson({ workOrder, criteria: delegationPlan.verifier.criteria, handoffs: outputs, synthesis: finalText }), modelContext);
3188
+ verification = validateVerifierResult(parseModelObject(verifierRaw, "workforce verifier"));
3189
+ receipt.verifier = {
3190
+ schemaVersion: "agentlas.workforce-verifier-receipt.v1",
3191
+ receiptId: verifierInvocationId,
3192
+ invocationId: verifierInvocationId,
3193
+ modelId: identity.modelId,
3194
+ runtimeId: identity.runtimeId,
3195
+ provider,
3196
+ status: "completed",
3197
+ agentReleaseId: verifierAssignment.agentReleaseId,
3198
+ startedAt: verifierStarted,
3199
+ completedAt: nowIso(D.now),
3200
+ inputSynthesisReceiptId: receipt.synthesis.receiptId,
3201
+ outputDigest: sha256(verification),
3202
+ result: verification,
3203
+ verdict: verification.status === "passed" ? "pass" : "fail",
3204
+ attempt: verifyAttempt,
3205
+ };
3206
+ if (verification.status === "passed") break;
3207
+ if (verifyAttempt === 1) {
3208
+ priorAttempt = { text: finalText, verification };
3209
+ receipt.correctiveHistory.push({
3210
+ synthesisReceiptId: synthesisInvocationId,
3211
+ verifierReceiptId: verifierInvocationId,
3212
+ synthesisOutputDigest: sha256(finalText),
3213
+ verification,
3214
+ });
3215
+ }
3216
+ }
3129
3217
 
3130
3218
  receipt.benchmarkAudit = auditBenchmarkReceipt(receipt);
3131
- if (verification.status !== "passed") fail("workforce_verification_failed", "pinned verifier rejected the synthesis", { issues: verification.issues });
3219
+ if (verification.status !== "passed") {
3220
+ fail("workforce_verification_failed", "pinned verifier rejected the synthesis twice (initial and one corrective retry)", {
3221
+ issues: verification.issues,
3222
+ correctiveRetryUsed: true,
3223
+ firstAttemptIssues: receipt.correctiveHistory[0]?.verification?.issues || [],
3224
+ });
3225
+ }
3132
3226
  if (ctx.benchmark === true && !receipt.benchmarkAudit.passed) fail("benchmark_receipt_incomplete", "benchmark mode requires planner, every child, synthesis, verifier, and no planner fallback", receipt.benchmarkAudit);
3133
3227
 
3134
3228
  receipt.status = "passed";
@@ -3236,10 +3330,20 @@ function create(deps = {}) {
3236
3330
  if (!ctx.silent) {
3237
3331
  // 서버/검증 거절 사유(details)는 영수증에만 남고 화면에서 누락되던 표시 결함 —
3238
3332
  // 정직 중계 원칙상 사유를 원문 그대로 병기한다(실사용 network 테스트에서 실증).
3239
- const detailText = receipt.failure.details
3240
- ? ` ${JSON.stringify(receipt.failure.details).slice(0, 600)}`
3333
+ // issues 배열은 JSON 캡에 잘리지 않도록 항목별로 온전히 표시한다.
3334
+ const details = receipt.failure.details;
3335
+ const issues = Array.isArray(details?.issues) ? details.issues : null;
3336
+ const otherDetails = details && typeof details === "object" && !Array.isArray(details)
3337
+ ? Object.fromEntries(Object.entries(details).filter(([key]) => key !== "issues"))
3338
+ : details;
3339
+ const detailText = otherDetails && (typeof otherDetails !== "object" || Object.keys(otherDetails).length)
3340
+ ? ` — ${JSON.stringify(otherDetails).slice(0, 1_200)}`
3241
3341
  : "";
3242
3342
  ui.error(`${receipt.failure.code}: ${receipt.failure.message}${detailText}`);
3343
+ if (issues) {
3344
+ for (const issue of issues.slice(0, 16)) ui.error(` - ${String(issue).slice(0, 400)}`);
3345
+ if (issues.length > 16) ui.error(` … ${issues.length - 16} more issues in the persisted receipt`);
3346
+ }
3243
3347
  }
3244
3348
  return { ok: false, error: receipt.failure, receipt, benchmarkArtifactPath };
3245
3349
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentlas",
3
- "version": "1.0.1",
4
- "description": "Agentlas agent terminal \u2014 chat with your installed AI agents and teams from the terminal, Claude Code style. Standalone: no desktop app required.",
3
+ "version": "1.0.2",
4
+ "description": "Agentlas agent terminal chat with your installed AI agents and teams from the terminal, Claude Code style. Standalone: no desktop app required.",
5
5
  "bin": {
6
6
  "agentlas": "bin/agentlas.cjs"
7
7
  },