agentlas 1.0.1 → 1.0.3
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 +56 -0
- package/engine/agentlas-workforce.cjs +185 -53
- package/engine/agentlas.cjs +3 -1
- package/engine/commands/index.cjs +26 -3
- package/engine/workforce/capture.cjs +7 -1
- package/engine/workforce/deps.cjs +7 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.3 — 2026-07-27
|
|
4
|
+
|
|
5
|
+
Live `workforce` runs surfaced four defects that no unit gate could reach.
|
|
6
|
+
|
|
7
|
+
- **A slash after Korean/Japanese/Chinese text no longer reads as a file
|
|
8
|
+
path.** The hub-boundary guard's absolute-path lookbehind excluded only
|
|
9
|
+
ASCII, so ordinary phrases ("진단/멱등키", "한국어/영어") were rejected as
|
|
10
|
+
private paths — and the phrase being the task itself meant no repair was
|
|
11
|
+
possible. Shared fixtures now pin this in both this engine and the server
|
|
12
|
+
(`scripts/sync-privacy-guard.sh`).
|
|
13
|
+
- **Bundle preparation is no longer killed by the connect timeout.** The
|
|
14
|
+
15s "connect" budget actually measured time-to-response-headers, and the
|
|
15
|
+
server computes a multi-slot roster before its first byte, so preparation
|
|
16
|
+
died as a transport error. Workforce calls now use their own budget.
|
|
17
|
+
- **Selection cycle rules match the Hub exactly.** Local validation only
|
|
18
|
+
checked handsOffTo/reportsTo, so a `reviews` cycle passed locally and came
|
|
19
|
+
back as a Hub rejection. All relations and self-edges now count.
|
|
20
|
+
- **A worker that exits non-zero reports its stdout tail too**, so a failure
|
|
21
|
+
whose stderr holds only unrelated warnings is no longer a dead end.
|
|
22
|
+
|
|
23
|
+
Also in this release:
|
|
24
|
+
|
|
25
|
+
- **Live narration**: a `workforce` run now prints the slot count and hub
|
|
26
|
+
menu size, the picked agent per slot by name, hub acceptance, each worker
|
|
27
|
+
as it starts, and the synthesis→verification transition.
|
|
28
|
+
- **One name per feature across platforms**: `hep-network`, `hep-cloud`,
|
|
29
|
+
`hep-build`, `hep-call`, `hep-search`, `hep-upload`, `hep-storm`,
|
|
30
|
+
`hep-browser`, `hep-connect` now work as terminal commands, matching the
|
|
31
|
+
skill names used from Claude Code and Codex. The typo guard suggests them.
|
|
32
|
+
|
|
33
|
+
## 1.0.2 — 2026-07-27
|
|
34
|
+
|
|
35
|
+
Workforce execution-contract fixes. Every worker in a `workforce` run now
|
|
36
|
+
knows its exact execution authority, and the run recovers honestly instead
|
|
37
|
+
of shipping broken output:
|
|
38
|
+
|
|
39
|
+
- Tool-less (no-authority) workers are told explicitly that zero tools are
|
|
40
|
+
granted and that the deliverable must be authored directly in the reply.
|
|
41
|
+
Previously a borrowed worker was silently stripped of tools, tried to
|
|
42
|
+
call them anyway, leaked raw tool-call markup into deliverables, and
|
|
43
|
+
produced empty output on content-type tasks.
|
|
44
|
+
- Worker handoffs are gated: leaked tool-call markup or an empty
|
|
45
|
+
deliverable triggers exactly one corrective re-run with a repair
|
|
46
|
+
directive; a repeat violation stops the run honestly with
|
|
47
|
+
`worker_output_contract_violation` (never a silent cleanup).
|
|
48
|
+
- A verifier rejection now triggers exactly one corrective synthesis pass
|
|
49
|
+
with the verifier's issues attached, then a re-verification. A second
|
|
50
|
+
rejection still fails honestly (`workforce_verification_failed`), now
|
|
51
|
+
reporting both attempts' issues.
|
|
52
|
+
- Selection handoff graphs are validated locally for circular
|
|
53
|
+
handsOffTo/reportsTo chains, so the structured repair loop fixes a cyclic
|
|
54
|
+
task force before the Hub sees it (previously a `task_force_cycle`
|
|
55
|
+
round-trip rejection).
|
|
56
|
+
- Failure display: verifier/server `issues` arrays are printed line by line
|
|
57
|
+
instead of being truncated inside a capped JSON blob.
|
|
58
|
+
|
|
3
59
|
## 1.0.1 — 2026-07-27
|
|
4
60
|
|
|
5
61
|
- 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;
|
|
@@ -91,7 +100,11 @@ const HUB_PATH_PATTERNS = [
|
|
|
91
100
|
/(?:^|[\s"'`()\[\]{}=:,;])~[/\\](?=\S)/,
|
|
92
101
|
/(?<![A-Za-z0-9])[A-Za-z]:[/\\](?=\S)/,
|
|
93
102
|
/(?:^|[\s"'`()\[\]{}=:,;])\\\\[^\\/\s]+[\\/][^\\/\s]+/,
|
|
94
|
-
|
|
103
|
+
// lookbehind가 ASCII만 제외하면 한글 단어 뒤 슬래시("진단/멱등키", "한국어/영어")가
|
|
104
|
+
// 절대경로로 오탐된다(2026-07-27 실측 — 한국어 워크오더 전멸 원인). 문자·숫자
|
|
105
|
+
// 전반(\p{L}\p{N})을 제외해 "A/B" 표기는 통과시키고, 공백·행머리 뒤 실제 경로는
|
|
106
|
+
// 그대로 잡는다.
|
|
107
|
+
/(?<![\p{L}\p{N}$])\/(?!\/|\s)(?:[^/\s"'`<>]+\/)*[^/\s"'`<>]+/u,
|
|
95
108
|
];
|
|
96
109
|
const HUB_SECRET_PATTERNS = [
|
|
97
110
|
["provider_token", /\b(?:sk-[A-Za-z0-9_-]{20,}|gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,})\b/],
|
|
@@ -1124,6 +1137,32 @@ function validateSelection(value, candidateSet, workOrder, identity, options = {
|
|
|
1124
1137
|
if (!["reportsTo", "handsOffTo", "reviews", "coordinatesWith"].includes(edge.relation)) fail("selection_invalid", "selection edge relation is invalid");
|
|
1125
1138
|
assertIds(edge.artifactKinds, "selection edge artifactKinds");
|
|
1126
1139
|
}
|
|
1140
|
+
// 엣지 사이클은 Hub validate가 task_force_cycle로 거절한다. 서버 규칙과 동일하게:
|
|
1141
|
+
// 관계 종류 불문 모든 엣지 + 자기참조가 사이클이다(reviews 맞교환도 거절 —
|
|
1142
|
+
// 2026-07-27 실측: handsOffTo만 검사하던 로컬 검증이 reviews 사이클을 통과시켜
|
|
1143
|
+
// 서버 거절로 되돌아왔다). 로컬에서 먼저 걸어야 재시도 루프가 왕복 없이 교정한다.
|
|
1144
|
+
{
|
|
1145
|
+
const adjacency = new Map();
|
|
1146
|
+
for (const edge of selection.edges) {
|
|
1147
|
+
if (edge.fromSlot === edge.toSlot) {
|
|
1148
|
+
fail("selection_invalid", `edges form a circular task force: ${edge.fromSlot} points at itself`);
|
|
1149
|
+
}
|
|
1150
|
+
if (!adjacency.has(edge.fromSlot)) adjacency.set(edge.fromSlot, []);
|
|
1151
|
+
adjacency.get(edge.fromSlot).push(edge.toSlot);
|
|
1152
|
+
}
|
|
1153
|
+
const states = new Map();
|
|
1154
|
+
const walk = (slot, trail) => {
|
|
1155
|
+
const state = states.get(slot);
|
|
1156
|
+
if (state === "done") return;
|
|
1157
|
+
if (state === "visiting") {
|
|
1158
|
+
fail("selection_invalid", `edges form a circular task force: ${[...trail, slot].join(" -> ")}`);
|
|
1159
|
+
}
|
|
1160
|
+
states.set(slot, "visiting");
|
|
1161
|
+
for (const next of adjacency.get(slot) || []) walk(next, [...trail, slot]);
|
|
1162
|
+
states.set(slot, "done");
|
|
1163
|
+
};
|
|
1164
|
+
for (const slot of adjacency.keys()) walk(slot, []);
|
|
1165
|
+
}
|
|
1127
1166
|
for (const releaseId of assertIds(selection.alternativesConsidered, "selection.alternativesConsidered")) {
|
|
1128
1167
|
if (!maps.all.has(releaseId)) fail("selection_invalid", `alternative ${releaseId} was outside the candidate set`);
|
|
1129
1168
|
}
|
|
@@ -1597,6 +1636,7 @@ function buildPrompts(task, identity) {
|
|
|
1597
1636
|
`Exact direct Selection example: ${stableJson(selectionShape)}`,
|
|
1598
1637
|
"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
1638
|
"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.",
|
|
1639
|
+
"edges must form an acyclic directed graph regardless of relation — reviews and coordinatesWith count too, and a slot may never point at itself. Never author a circular chain (for example A reviews B while B reviews A); the Hub rejects circular task forces.",
|
|
1600
1640
|
].join("\n");
|
|
1601
1641
|
const plannerSchemaRequirements = [
|
|
1602
1642
|
`Return exactly one object: ${stableJson(plannerShape)}`,
|
|
@@ -2669,10 +2709,28 @@ function create(deps = {}) {
|
|
|
2669
2709
|
workOrderInvocationId,
|
|
2670
2710
|
};
|
|
2671
2711
|
|
|
2712
|
+
// 실황 내레이션: 사용자는 "누가 소집됐고 지금 뭘 하는지"를 보면서 신뢰를
|
|
2713
|
+
// 형성한다(2026-07-27 오너 요구). 결과에 영향 없는 표시 전용 — silent 존중.
|
|
2714
|
+
if (!ctx.silent) {
|
|
2715
|
+
const nameByRelease = new Map();
|
|
2716
|
+
for (const slotRow of candidateSet.slots) {
|
|
2717
|
+
for (const cand of slotRow.candidates) nameByRelease.set(cand.agentReleaseId, cand.name || cand.agentReleaseId);
|
|
2718
|
+
}
|
|
2719
|
+
const menuCount = candidateSet.slots.reduce((sum, slotRow) => sum + slotRow.candidates.length, 0);
|
|
2720
|
+
ui.info(ui.lang === "ko"
|
|
2721
|
+
? `워크오더 ${workOrder.roleSlots.length}슬롯 · 허브 후보 ${menuCount}명 메뉴 수신`
|
|
2722
|
+
: `work order: ${workOrder.roleSlots.length} slot(s) · hub menu of ${menuCount} candidates`);
|
|
2723
|
+
for (const row of selection.assignments) {
|
|
2724
|
+
ui.info(ui.lang === "ko"
|
|
2725
|
+
? ` 선발 ${row.slotId} ← ${nameByRelease.get(row.agentReleaseId) || row.agentReleaseId}`
|
|
2726
|
+
: ` picked ${row.slotId} ← ${nameByRelease.get(row.agentReleaseId) || row.agentReleaseId}`);
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2672
2729
|
const validationRaw = await hubStage("workforce.validate_selection", { workOrder, candidateSet, selection });
|
|
2673
2730
|
validationReceipt = validateSelectionReceipt(validationRaw, selection, candidateSet, workOrder);
|
|
2674
2731
|
benchmarkState.selectionValidation = validationReceipt;
|
|
2675
2732
|
receipt.selectionReceiptId = validationReceipt.selectionReceiptId;
|
|
2733
|
+
if (!ctx.silent) ui.info(ui.lang === "ko" ? "허브 검증 수락 — 번들 준비 중" : "hub validation accepted — preparing bundles");
|
|
2676
2734
|
|
|
2677
2735
|
const preparedRaw = await hubStage("workforce.prepare_execution", { workOrder, candidateSet, selection, validationReceipt });
|
|
2678
2736
|
({ prepared, rosterByPair } = validatePreparedExecution(preparedRaw, workOrder, selection, candidateSet, validationReceipt));
|
|
@@ -2864,7 +2922,13 @@ function create(deps = {}) {
|
|
|
2864
2922
|
|
|
2865
2923
|
const runPinnedInvocation = async ({ pinned, system, prompt, label, grantedToolIds, extra = {} }) => {
|
|
2866
2924
|
const invocationId = `workforce-invocation:${crypto.randomUUID()}`;
|
|
2867
|
-
|
|
2925
|
+
// 워커는 도구 상태를 스스로 알 수 없다. 고지 없이 잠그면 존재하지 않는 도구를
|
|
2926
|
+
// 부르다 호출 문법이 산출물에 그대로 새고, 코드 저장소 워크플로를 가정한 채
|
|
2927
|
+
// 본 작업 없이 끝난다(2026-07-27 실측). 결정적 문자열만 사용(3-OS 바이트 패리티).
|
|
2928
|
+
const authorityDirective = grantedToolIds.length
|
|
2929
|
+
? `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.`
|
|
2930
|
+
: "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.";
|
|
2931
|
+
const text = assertString(await runModel(runtime, [system, authorityDirective].join("\n\n"), prompt, {
|
|
2868
2932
|
...modelContext,
|
|
2869
2933
|
authorityMode: grantedToolIds.length ? "policy-filtered" : "no-authority",
|
|
2870
2934
|
grantedToolIds,
|
|
@@ -2887,6 +2951,31 @@ function create(deps = {}) {
|
|
|
2887
2951
|
};
|
|
2888
2952
|
};
|
|
2889
2953
|
|
|
2954
|
+
// 핸드오프 산출물 전용 게이트: 도구 마크업/빈 산출물이면 교정 지시로 1회 재실행,
|
|
2955
|
+
// 재발 시 조용한 완화 대신 정직 정지(silent-default 금지 원칙).
|
|
2956
|
+
const runHandoffInvocation = async (args) => {
|
|
2957
|
+
const first = await runPinnedInvocation(args);
|
|
2958
|
+
const violation = handoffContractViolation(first.text);
|
|
2959
|
+
if (!violation) return first;
|
|
2960
|
+
const repairDirective = violation === "tool_markup"
|
|
2961
|
+
? "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."
|
|
2962
|
+
: "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.";
|
|
2963
|
+
const retried = await runPinnedInvocation({
|
|
2964
|
+
...args,
|
|
2965
|
+
system: [args.system, repairDirective].join("\n\n"),
|
|
2966
|
+
extra: { ...(args.extra || {}), handoffContractRetry: violation },
|
|
2967
|
+
});
|
|
2968
|
+
const repeat = handoffContractViolation(retried.text);
|
|
2969
|
+
if (repeat) {
|
|
2970
|
+
fail("worker_output_contract_violation", `${args.label} kept violating the handoff contract (${repeat}) after one corrective retry`, {
|
|
2971
|
+
violation: repeat,
|
|
2972
|
+
firstViolation: violation,
|
|
2973
|
+
label: args.label,
|
|
2974
|
+
});
|
|
2975
|
+
}
|
|
2976
|
+
return retried;
|
|
2977
|
+
};
|
|
2978
|
+
|
|
2890
2979
|
const runNestedManagerPlan = async ({ pinned, packet, grantedToolIds }) => {
|
|
2891
2980
|
const graph = pinned.executionGraph;
|
|
2892
2981
|
const exactWorkerIds = graph.workers.map((row) => row.id);
|
|
@@ -2933,6 +3022,7 @@ function create(deps = {}) {
|
|
|
2933
3022
|
const packet = delegationPlan.packets[index];
|
|
2934
3023
|
const pair = `${packet.slotId}\0${packet.agentReleaseId}`;
|
|
2935
3024
|
const pinned = rosterByPair.get(pair);
|
|
3025
|
+
if (!ctx.silent) ui.info(ui.lang === "ko" ? ` 워커 실행 중: ${packet.slotId}` : ` worker running: ${packet.slotId}`);
|
|
2936
3026
|
const capabilityBindings = bindingsByPair.get(pair) || [];
|
|
2937
3027
|
const grantedToolIds = [...new Set(capabilityBindings.map((row) => row.toolId))].sort();
|
|
2938
3028
|
const startedAt = nowIso(D.now);
|
|
@@ -2941,7 +3031,7 @@ function create(deps = {}) {
|
|
|
2941
3031
|
let directInvocation = null;
|
|
2942
3032
|
let nestedExecutionId = null;
|
|
2943
3033
|
if (pinned.entityKind === "agent") {
|
|
2944
|
-
const direct = await
|
|
3034
|
+
const direct = await runHandoffInvocation({
|
|
2945
3035
|
pinned,
|
|
2946
3036
|
grantedToolIds,
|
|
2947
3037
|
label: `worker ${packet.packetId}`,
|
|
@@ -2962,7 +3052,7 @@ function create(deps = {}) {
|
|
|
2962
3052
|
const manager = await runNestedManagerPlan({ pinned, packet, grantedToolIds });
|
|
2963
3053
|
const graphWorkerOutputs = await Promise.all(pinned.executionGraph.workers.map(async (graphWorker, workerIndex) => {
|
|
2964
3054
|
const graphPacket = manager.plan.packets[workerIndex];
|
|
2965
|
-
const invoked = await
|
|
3055
|
+
const invoked = await runHandoffInvocation({
|
|
2966
3056
|
pinned,
|
|
2967
3057
|
grantedToolIds,
|
|
2968
3058
|
label: `nested worker ${graphWorker.id}`,
|
|
@@ -2978,7 +3068,7 @@ function create(deps = {}) {
|
|
|
2978
3068
|
});
|
|
2979
3069
|
return { graphWorker, graphPacket, text: invoked.text, invocation: invoked.invocation };
|
|
2980
3070
|
}));
|
|
2981
|
-
const managerSynthesis = await
|
|
3071
|
+
const managerSynthesis = await runHandoffInvocation({
|
|
2982
3072
|
pinned,
|
|
2983
3073
|
grantedToolIds,
|
|
2984
3074
|
label: `nested manager synthesis ${packet.packetId}`,
|
|
@@ -3080,55 +3170,87 @@ function create(deps = {}) {
|
|
|
3080
3170
|
if (rejectedWorker) throw rejectedWorker.reason;
|
|
3081
3171
|
|
|
3082
3172
|
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
3173
|
const verifierAssignment = selection.assignments.find((row) => row.slotId === delegationPlan.verifier.slotId && row.agentReleaseId === delegationPlan.verifier.agentReleaseId);
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3174
|
+
// 검증자가 불합격을 내면 그 지적을 들고 합성을 1회 교정 후 재검증한다.
|
|
3175
|
+
// 첫 시도의 영수증도 correctiveHistory로 보존한다(감사 추적 진실성).
|
|
3176
|
+
let finalText = null;
|
|
3177
|
+
let verification = null;
|
|
3178
|
+
let synthesisInvocationId = null;
|
|
3179
|
+
let verifierInvocationId = null;
|
|
3180
|
+
let priorAttempt = null;
|
|
3181
|
+
receipt.correctiveHistory = [];
|
|
3182
|
+
if (!ctx.silent) ui.info(ui.lang === "ko" ? "합성 → 검증 단계" : "synthesis → verification");
|
|
3183
|
+
for (let verifyAttempt = 1; verifyAttempt <= 2; verifyAttempt += 1) {
|
|
3184
|
+
const synthesisStarted = nowIso(D.now);
|
|
3185
|
+
synthesisInvocationId = `workforce-invocation:${crypto.randomUUID()}`;
|
|
3186
|
+
finalText = assertString(await runModel(runtime, [
|
|
3187
|
+
"You are the top-level host LLM synthesizer for this immutable Agentlas workforce run.",
|
|
3188
|
+
"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.",
|
|
3189
|
+
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." : "",
|
|
3190
|
+
].filter(Boolean).join("\n\n"), stableJson(verifyAttempt > 1
|
|
3191
|
+
? { workOrder, synthesis: delegationPlan.synthesis, handoffs: outputs, priorSynthesis: priorAttempt.text, verifierRejection: priorAttempt.verification }
|
|
3192
|
+
: { workOrder, synthesis: delegationPlan.synthesis, handoffs: outputs }), modelContext), "synthesis output", 1_000_000);
|
|
3193
|
+
receipt.synthesis = {
|
|
3194
|
+
schemaVersion: "agentlas.workforce-synthesis-receipt.v1",
|
|
3195
|
+
receiptId: synthesisInvocationId,
|
|
3196
|
+
invocationId: synthesisInvocationId,
|
|
3197
|
+
modelId: identity.modelId,
|
|
3198
|
+
runtimeId: identity.runtimeId,
|
|
3199
|
+
provider,
|
|
3200
|
+
status: "completed",
|
|
3201
|
+
agentReleaseId: synthesisAssignment.agentReleaseId,
|
|
3202
|
+
startedAt: synthesisStarted,
|
|
3203
|
+
completedAt: nowIso(D.now),
|
|
3204
|
+
inputChildReceiptIds: receipt.workers.filter((row) => row.status === "completed").map((row) => row.receiptId),
|
|
3205
|
+
outputDigest: sha256(finalText),
|
|
3206
|
+
attempt: verifyAttempt,
|
|
3207
|
+
};
|
|
3208
|
+
|
|
3209
|
+
const verifierStarted = nowIso(D.now);
|
|
3210
|
+
verifierInvocationId = `workforce-invocation:${crypto.randomUUID()}`;
|
|
3211
|
+
const verifierRaw = await runModel(runtime, [
|
|
3212
|
+
"You are the top-level host LLM verifier for this Agentlas workforce run.",
|
|
3213
|
+
'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":[]}.',
|
|
3214
|
+
"Use double-quoted valid JSON. Passing requires evidence for every criterion; do not rubber-stamp.",
|
|
3215
|
+
].join("\n\n"), stableJson({ workOrder, criteria: delegationPlan.verifier.criteria, handoffs: outputs, synthesis: finalText }), modelContext);
|
|
3216
|
+
verification = validateVerifierResult(parseModelObject(verifierRaw, "workforce verifier"));
|
|
3217
|
+
receipt.verifier = {
|
|
3218
|
+
schemaVersion: "agentlas.workforce-verifier-receipt.v1",
|
|
3219
|
+
receiptId: verifierInvocationId,
|
|
3220
|
+
invocationId: verifierInvocationId,
|
|
3221
|
+
modelId: identity.modelId,
|
|
3222
|
+
runtimeId: identity.runtimeId,
|
|
3223
|
+
provider,
|
|
3224
|
+
status: "completed",
|
|
3225
|
+
agentReleaseId: verifierAssignment.agentReleaseId,
|
|
3226
|
+
startedAt: verifierStarted,
|
|
3227
|
+
completedAt: nowIso(D.now),
|
|
3228
|
+
inputSynthesisReceiptId: receipt.synthesis.receiptId,
|
|
3229
|
+
outputDigest: sha256(verification),
|
|
3230
|
+
result: verification,
|
|
3231
|
+
verdict: verification.status === "passed" ? "pass" : "fail",
|
|
3232
|
+
attempt: verifyAttempt,
|
|
3233
|
+
};
|
|
3234
|
+
if (verification.status === "passed") break;
|
|
3235
|
+
if (verifyAttempt === 1) {
|
|
3236
|
+
priorAttempt = { text: finalText, verification };
|
|
3237
|
+
receipt.correctiveHistory.push({
|
|
3238
|
+
synthesisReceiptId: synthesisInvocationId,
|
|
3239
|
+
verifierReceiptId: verifierInvocationId,
|
|
3240
|
+
synthesisOutputDigest: sha256(finalText),
|
|
3241
|
+
verification,
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3129
3245
|
|
|
3130
3246
|
receipt.benchmarkAudit = auditBenchmarkReceipt(receipt);
|
|
3131
|
-
if (verification.status !== "passed")
|
|
3247
|
+
if (verification.status !== "passed") {
|
|
3248
|
+
fail("workforce_verification_failed", "pinned verifier rejected the synthesis twice (initial and one corrective retry)", {
|
|
3249
|
+
issues: verification.issues,
|
|
3250
|
+
correctiveRetryUsed: true,
|
|
3251
|
+
firstAttemptIssues: receipt.correctiveHistory[0]?.verification?.issues || [],
|
|
3252
|
+
});
|
|
3253
|
+
}
|
|
3132
3254
|
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
3255
|
|
|
3134
3256
|
receipt.status = "passed";
|
|
@@ -3236,10 +3358,20 @@ function create(deps = {}) {
|
|
|
3236
3358
|
if (!ctx.silent) {
|
|
3237
3359
|
// 서버/검증 거절 사유(details)는 영수증에만 남고 화면에서 누락되던 표시 결함 —
|
|
3238
3360
|
// 정직 중계 원칙상 사유를 원문 그대로 병기한다(실사용 network 테스트에서 실증).
|
|
3239
|
-
|
|
3240
|
-
|
|
3361
|
+
// issues 배열은 JSON 캡에 잘리지 않도록 항목별로 온전히 표시한다.
|
|
3362
|
+
const details = receipt.failure.details;
|
|
3363
|
+
const issues = Array.isArray(details?.issues) ? details.issues : null;
|
|
3364
|
+
const otherDetails = details && typeof details === "object" && !Array.isArray(details)
|
|
3365
|
+
? Object.fromEntries(Object.entries(details).filter(([key]) => key !== "issues"))
|
|
3366
|
+
: details;
|
|
3367
|
+
const detailText = otherDetails && (typeof otherDetails !== "object" || Object.keys(otherDetails).length)
|
|
3368
|
+
? ` — ${JSON.stringify(otherDetails).slice(0, 1_200)}`
|
|
3241
3369
|
: "";
|
|
3242
3370
|
ui.error(`${receipt.failure.code}: ${receipt.failure.message}${detailText}`);
|
|
3371
|
+
if (issues) {
|
|
3372
|
+
for (const issue of issues.slice(0, 16)) ui.error(` - ${String(issue).slice(0, 400)}`);
|
|
3373
|
+
if (issues.length > 16) ui.error(` … ${issues.length - 16} more issues in the persisted receipt`);
|
|
3374
|
+
}
|
|
3243
3375
|
}
|
|
3244
3376
|
return { ok: false, error: receipt.failure, receipt, benchmarkArtifactPath };
|
|
3245
3377
|
}
|
package/engine/agentlas.cjs
CHANGED
|
@@ -127,7 +127,9 @@ function main() {
|
|
|
127
127
|
*/
|
|
128
128
|
if (!agent && normalized.length === 1 && /^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(normalized[0])) {
|
|
129
129
|
const token = normalized[0];
|
|
130
|
-
const names = Object.keys(commands.COMMANDS)
|
|
130
|
+
const names = Object.keys(commands.COMMANDS)
|
|
131
|
+
.concat(Object.keys(commands.COMMAND_ALIASES || {}))
|
|
132
|
+
.concat(commands.NOT_YET_PORTED || []);
|
|
131
133
|
const near = nearestCommands(token, names);
|
|
132
134
|
const ko = ctx.lang === "ko";
|
|
133
135
|
ctx.err(ko
|
|
@@ -103,9 +103,32 @@ const DESKTOP_ONLY_SURFACES = {
|
|
|
103
103
|
// 무인자 호출이 프롬프트로 오라우팅되면 안 되는 명령 (smoke 가드 대상)
|
|
104
104
|
const GUARDED_NO_ARG = new Set(["search", "install", "upload"]);
|
|
105
105
|
|
|
106
|
+
// 플랫폼 간 이름 통일(오너 결정 2026-07-27): 클로드코드/코덱스에서 부르는 hep-*
|
|
107
|
+
// 스킬명과 터미널 명령이 서로 다르면 사용자가 어느 표면에 있는지에 따라 이름을
|
|
108
|
+
// 바꿔 써야 한다. 같은 기능은 어디서든 같은 이름으로 부른다.
|
|
109
|
+
const COMMAND_ALIASES = {
|
|
110
|
+
"hep-network": "workforce",
|
|
111
|
+
network: "workforce",
|
|
112
|
+
"hep-cloud": "cloud",
|
|
113
|
+
"hep-build": "build",
|
|
114
|
+
"hep-call": "call",
|
|
115
|
+
"hep-search": "search",
|
|
116
|
+
"hep-upload": "upload",
|
|
117
|
+
"hep-storm": "storm",
|
|
118
|
+
"hep-browser": "browser",
|
|
119
|
+
"hep-connect": "connect",
|
|
120
|
+
"hep-local": "workforce",
|
|
121
|
+
"hep-hub": "search",
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
function resolveCommandName(cmd) {
|
|
125
|
+
return COMMAND_ALIASES[cmd] || cmd;
|
|
126
|
+
}
|
|
127
|
+
|
|
106
128
|
function dispatch(ctx, argv) {
|
|
107
|
-
const [
|
|
108
|
-
if (!
|
|
129
|
+
const [rawCmd, ...rest] = argv;
|
|
130
|
+
if (!rawCmd) return null; // 엔진이 REPL로 진입
|
|
131
|
+
const cmd = resolveCommandName(rawCmd);
|
|
109
132
|
|
|
110
133
|
if (COMMANDS[cmd]) {
|
|
111
134
|
return COMMANDS[cmd]().run(ctx, rest);
|
|
@@ -133,4 +156,4 @@ function dispatch(ctx, argv) {
|
|
|
133
156
|
return undefined; // 알 수 없는 토큰 — 엔진이 에이전트 이름/프롬프트로 해석 시도
|
|
134
157
|
}
|
|
135
158
|
|
|
136
|
-
module.exports = { dispatch, COMMANDS, NOT_YET_PORTED, GUARDED_NO_ARG, DESKTOP_ONLY_SURFACES };
|
|
159
|
+
module.exports = { dispatch, COMMANDS, COMMAND_ALIASES, resolveCommandName, NOT_YET_PORTED, GUARDED_NO_ARG, DESKTOP_ONLY_SURFACES };
|
|
@@ -359,7 +359,13 @@ function captureRuntime(kind, systemPrompt, prompt, opts) {
|
|
|
359
359
|
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
360
360
|
const stderr = Buffer.concat(stderrChunks).toString("utf8");
|
|
361
361
|
if (code && code !== 0) {
|
|
362
|
-
|
|
362
|
+
// stderr가 경고문뿐이면 진짜 원인이 stdout(JSON 오류 응답 등)에 있을 수 있다 —
|
|
363
|
+
// 2026-07-27 실측: 워커 exit 1이 설정 경고 2줄만 남기고 원인 불명이 됐다.
|
|
364
|
+
// 두 스트림의 꼬리를 모두 싣는다.
|
|
365
|
+
const stdoutTail = stdout.trim().slice(-400);
|
|
366
|
+
finishReject(new Error(
|
|
367
|
+
`${kind} exited ${code}: ${stderr.slice(-500)}${stdoutTail ? `\n--- stdout tail ---\n${stdoutTail}` : ""}`,
|
|
368
|
+
));
|
|
363
369
|
return;
|
|
364
370
|
}
|
|
365
371
|
const raw = stdout.trim() || stderr.trim();
|
|
@@ -425,7 +425,13 @@ function buildWorkforceDeps(ctx = {}) {
|
|
|
425
425
|
// v1과 동일: callHubTool은 주입하지 않는다. 워크포스 모듈 내부의 jsonrpc 경로가
|
|
426
426
|
// 거절 코드 원문 전파·retryClass 계약을 소유하며, fetchHub는 버퍼드
|
|
427
427
|
// {ok,status,headers,text} 어댑터 형태를 만족한다(3중 타임아웃 + 16MB 상한).
|
|
428
|
-
|
|
428
|
+
// 워크포스 전용 타임아웃: prepare_execution은 서버가 로스터 번들을 조립하는 동안
|
|
429
|
+
// 첫 바이트 없이 계산한다(1슬롯 실측 7.2s, 다슬롯은 그 배수). 기본 connect 15s는
|
|
430
|
+
// 실제로는 "응답 헤더까지"를 재므로 다슬롯 준비를 처형한다(2026-07-27 전송오류
|
|
431
|
+
// 2연속의 진범). 준비 상한을 여유 있게 준다 — idle/total 계약은 유지.
|
|
432
|
+
fetchHub: (url, init) => hubClient.fetchHub(url, init, {
|
|
433
|
+
timeoutConfig: { connectMs: 120_000, idleMs: 60_000, totalMs: 300_000 },
|
|
434
|
+
}),
|
|
429
435
|
resolveRuntime: resolveWorkforceRuntime,
|
|
430
436
|
captureRuntime: capture.captureRuntime,
|
|
431
437
|
runApi: capture.runApi,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlas",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Agentlas agent terminal
|
|
3
|
+
"version": "1.0.3",
|
|
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
|
},
|