Skip to content

Run several delegations as one task graph - #85

Merged
Seungpyo1007 merged 3 commits into
developfrom
feature/orchestra
Sep 3, 2026
Merged

Run several delegations as one task graph#85
Seungpyo1007 merged 3 commits into
developfrom
feature/orchestra

Conversation

@Seungpyo1007

@Seungpyo1007 Seungpyo1007 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Adds a layer above single delegation: a task graph.

Until now a delegation ran one at a time. There was no place to say "run A and B together, and when both finish hand their results to C" — the manager had to delegate, wait for the round to come back, read the results, and delegate again.

The engine

src/engine/orchestra.ts contains no Promise, no timer, and no await. It is a state machine the caller drives: ask what may start (ready), say it started (start), say it finished (settle). Every race is therefore reproducible in a test where no time passes, and the directory's import-zero rule holds.

The point of the layer is refusing to lose work:

  • When a dependency breaks, everything waiting on it is closed with a reason, immediately. Direct cause (dep-failed) and knocked-over-next (dep-skipped) are distinguished, so one failure blocking twenty tasks still reports one cause rather than twenty.
  • A task that ran but returned nothing is kept apart from one that succeeded. Dependents still proceed, but the report does not call it done, and the empty result is passed downstream as an explicit blank rather than dropped from the list — dropping it reads to the next agent as "that task never existed".
  • A delegation that timed out is a failure. Letting dependents run on an answer that never arrived is worse than blocking them.
  • A graph that does not hold together starts nothing. Half-running it and discovering the cycle afterwards cannot be undone. The cycle error carries the ids caught in the loop; an unknown dependency carries its name.

The tool

delegate_graph takes {id, agent, task, needs} entries. Results from earlier tasks are appended to the seed prompt of later ones automatically.

Two policy limits are relaxed inside a graph and one is not. Per-turn count and the duplicate-target ban existed to bound a model delegating without a plan; a graph is that plan, validated before it runs and visible on screen, so graph size (12) replaces them. Concurrency stays capped — that one is file locks and billing, not taste.

startDelegation returned only a rendered string, so a graph could not tell success from refusal. It now returns the outcome alongside the text, and refusals become failed outcomes that close dependents with a reason.

Found while probing the running app

Two tasks for the same agent ran concurrently (peak=2). Choosing a batch and dispatching it were separate loops, so an agent was marked busy too late and two tasks for it passed side by side within one batch. Overlapping makes the layer below refuse with agent-busy, and that refusal spreads as a failure — killing tasks whose only problem was ordering.

Unit tests cannot see this: Orchestra deliberately leaves concurrency to its caller, so a caller's mistake is invisible to Orchestra's own tests. Reproduced in the running app, fixed, re-confirmed.

Also

PlanResult uses a string discriminant rather than ok: boolean — the root tsconfig is strict:false, where a boolean union does not narrow and r.error fails to compile. policy.ts documents the same trap for PolicyDecision.

The orch dictionary is read by the model, not shown on screen. A missing key would send the raw key name as the result, and the manager would summarize that to the user — failure that does not look like failure. A test pins the keys, all four languages, and matching placeholders.

Verification

  • 38 new unit tests (1171 total, up from 1133); typecheck clean under both tsconfigs.
  • 14/14 in a CDP probe against the running app: fan-out then merge, results carried forward, concurrency cap, same-agent avoidance, failure cascade with reasons, cycle and unknown-dependency refusal, size cap, empty list, and no raw dictionary keys reaching the model.

The panel

Tool cards alone did not show what was waiting on what — five cards sit side by side, and a graph where one task killed the rest looks identical to one that was simply ordered that way.

planTasks now returns the waves Kahn's algorithm builds anyway rather than discarding them; order alone cannot express "run these three together, then this one".

The panel uses its own icon set rather than the plan panel's. Not-yet-run () and never-will-run () must not look alike, and ran-but-returned-nothing () must not look like done. A blocked row carries what blocked it, inline — greyed out on its own, waiting and blocked read the same.

Starting a graph switches the left panel to the workflow tab. Opening a project leaves it on the tree, so without this the screen says nothing at all while several agents run. The demo path already does this for the same reason.

Five probe checks failed during this work and all five were the probe's fault, not the code's: the left panel was on the tree tab, and the selector matched the card's header rather than the card (the header also begins with the panel's label). Printing state.graph directly showed the wiring was correct and only the path to the screen was wrong.

Probe now at 18/18, including the tab switch — the probe no longer clicks the tab itself.

지금까지는 위임이 한 건씩만 돌았음. 여럿을 엮어 "A·B 동시에 돌리고 둘 다
끝나면 C 에게 결과 넘기기" 를 만들 자리가 없었음.

Promise·타이머 안 씀. 부르는 쪽이 손으로 돌리는 상태 기계 — ready/start/settle.
시간이 안 흐르는 테스트에서 경합을 그대로 재현할 수 있고 engine/ 의 import 0 도 지킴.
동시 상한은 계속 policy.ts 가 관장.

핵심은 안 돈 것을 없던 일로 안 만드는 것:
- 의존이 무너지면 뒤엣것들이 이유와 함께 skipped 로 닫힘
- 직접 원인(dep-failed)과 딸려 막힌 것(dep-skipped)을 구분 — 스무 개가 막혀도 원인은 하나
- 돌았는데 아무 말 없는 것(empty)을 성공과 안 섞음
- 답 못 받은 것(timeout)은 실패로 봄. 뒤가 빈 결과 받고 도는 것보다 막히는 게 나음

사이클은 순환에 낀 id 를 실어서 냄. "어딘가 순환" 만으로는 못 고침.

무너진 의존 판정이 satisfied 와 cascade 두 겹인데, cascade 가 먼저 닫아서
테스트로 구분이 안 됨. 지우면 막힌 작업이 pending 인 채 굳어 finished() 가
영영 안 참이 됨 — 그 이유를 코드에 적어 둠.
관리자가 위임 여러 건을 의존 관계까지 얹어 한 번에 올릴 수 있음.
의존 없는 것끼리 동시에 돌고, 앞선 작업의 답은 뒤 작업 씨앗에 자동으로 실림 —
결과 기다렸다 다시 위임할 필요 없음.

정책은 그래프 안에서만 완화. per-turn 상한과 중복 대상 금지는 계획 없이
위임하는 모델을 묶던 것이고 그래프가 그 계획 자체라, 그래프 크기(12개)로 대신함.
동시 실행 상한은 안 품 — 파일 락과 요금이 걸린 진짜 한계임.

못 쓸 그래프는 한 개도 안 태움. 반쯤 돌려 놓고 사이클을 찾으면 되돌릴 수 없음.
사이클은 낀 id 를, 없는 의존은 그 이름을 실어 보냄.

startDelegation 이 문자열만 내던 걸 결과까지 같이 내게 고침. 그래프가 성공과
거절을 구분할 방법이 없었음. 거절도 실패 결과로 만들어 뒤엣것들이 이유와 함께 닫힘.

실제 앱 프로브에서 하나 잡음: 같은 에이전트 둘이 겹쳐 돌았음(peak=2).
배치를 고르는 루프와 태우는 루프가 나뉘어 있어 busy 표시가 늦었고, 한 배치 안의
같은 대상 둘이 나란히 통과했음. 겹치면 아래층이 agent-busy 로 거절하고 그게
실패로 번져서 순서만 안 맞았을 뿐인 작업들이 통째로 죽음. 단위 테스트로는
안 잡힘 — Orchestra 는 동시성을 부르는 쪽에 맡기므로 부르는 쪽 실수가 안 보임.

PlanResult 를 ok: boolean 에서 kind 문자열로 바꿈. 루트 tsconfig 가 strict:false 라
불리언 유니온이 안 좁혀짐. policy.ts 의 PolicyDecision 이 같은 이유로 같은 모양임.

orch 사전은 모델이 읽음. 키가 빠지면 날 키가 그대로 결과로 가고 관리자가 그걸
요약함 — 실패가 실패로 안 보이는 가장 나쁜 모양이라 테스트로 막음.
@Seungpyo1007 Seungpyo1007 added the enhancement New feature or request label Aug 28, 2026
@Seungpyo1007 Seungpyo1007 self-assigned this Aug 28, 2026
@Seungpyo1007 Seungpyo1007 added this to the v0.3 (2026-09-30) milestone Aug 28, 2026
도구 카드만으로는 뭐가 뭘 기다리는지가 안 보였음. 카드 다섯 장이 따로 떠 있을
뿐이라, 하나가 죽어서 나머지가 안 돈 건지 원래 순서가 그런 건지 구분이 안 됨.

planTasks 가 Kahn 돌면서 어차피 만들던 단(wave)을 버리지 않고 냄. 순서만으로는
"이 셋을 같이 돌리고 끝나면 이것" 을 못 그림.

패널 아이콘을 계획 패널과 따로 둠. 아직 안 돈 것(○)과 영영 안 돌 것(⊘)이 같아
보이면 안 되고, 돌았는데 결과 없는 것(◌)도 완료와 갈라야 함. 막힌 줄에는 무엇에
막혔는지를 같이 적음 — 회색으로만 두면 대기 중인 것과 구분이 안 됨.

그래프가 시작하면 흐름 탭으로 옮김. 프로젝트를 열면 트리 탭에 가 있어서, 안 바꾸면
여러 에이전트가 도는 내내 화면이 아무 말도 안 함. 데모 경로가 같은 이유로 같은 일을 함.

프로브에서 다섯 건이 실패했는데 전부 프로브 잘못이었음 — 좌측이 트리 탭이었고,
셀렉터가 카드 대신 헤더를 집었음(헤더도 "작업 그래프" 로 시작). state.graph 를
직접 찍어 보고 배선은 멀쩡하고 도달 경로만 틀렸다는 걸 확인함.
@Seungpyo1007
Seungpyo1007 merged commit 20b55ff into develop Sep 3, 2026
1 check passed
@Seungpyo1007
Seungpyo1007 deleted the feature/orchestra branch September 3, 2026 05:27
@Seungpyo1007 Seungpyo1007 mentioned this pull request Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant