hor-agent-loop · git:20260905.5a04969 · 2026-09-05 · sha256 dbace9e4bc72b7fc
hor-agent-loop git:20260905.5a04969A
Immutable. This exact content never changes and is served at /api/v1/blob/dbace9e4bc72b7fc.
---
name: hor-agent-loop
description: >
Build an LLM agent loop (iterative agent) in a backend repository with the three
@openreachtech/mentsu-agent-loop packages — core (the transport-agnostic iteration engine),
renchan-job (run a loop as a BullMQ/Redis job with progress publishing), and graphql (a mutation
that launches the loop plus a subscription that streams its progress). Use whenever the user
asks to add or edit an agent loop, an agent action, a composite agent, or to stream agent
progress.
---
# Agent Loop (building agent loops with mentsu-agent-loop)
A skill for implementing an LLM agent's iteration ("loop") in a backend. The iteration engine,
progress delivery, composition, job execution, and GraphQL integration are all provided by the
**three `@openreachtech/mentsu-agent-loop-*` packages**. The app writes only the domain-specific
**actions and loops**; iteration, stopping, compression, parallelism, and transport ride on the
ready-made base classes.
This skill covers the libraries' specs and use cases, plus the integration patterns for renchan's
GraphQL and renchan-job-bullmq. **Always use these three packages** (never hand-roll your own
iteration engine, queue bridge, or progress channel). The details are split into the [detail
files](#detail-files) at the end.
## Grand principle: write actions and loops without knowing about AI, DB, queues, or GraphQL (one-way dependency)
The center of this package family's design is a **one-way dependency direction (outer → core)**.
The iteration logic the app writes (actions / loops) **imports no business dependency and no
transport**; everything is reached through the **`context` (an opaque DI seam)** and through **thin
adapters**.
| Layer | Package | Responsibility | Depends on |
| --- | --- | --- | --- |
| **core** | `@openreachtech/mentsu-agent-loop-core` | Iteration engine (Action / Loop / composition / Runner / Topic). **Zero dependencies** | imports nothing |
| **job execution** | `@openreachtech/mentsu-agent-loop-renchan-job` | Run a loop as a renchan-job / BullMQ Redis job and publish progress | → core, renchan-job-bullmq |
| **GraphQL** | `@openreachtech/mentsu-agent-loop-graphql` | Launch mutation + progress-subscription | → core, renchan |
| **app** | (this repository) | Actions / loops / wiring | → all of the above |
- **Why keep the dependency one-way**: the moment core imports AI/DB/queue/transport, the iteration
logic is bound to a specific backend and can no longer be tested in-process or switched between
execution modes. Keeping core dependency-free and receiving dependencies from `context` means **the
same loop runs in-process or as a job, and tests just swap `context` for a stub**.
- **Why write only actions and loops**: the iteration body (the tail-recursive `iterate`), the
progress seam (`emitProgress`), composition (parallel / sequential), the queue bridge, and channel
naming are all **shared machinery**; writing them per app guarantees drift. Push them into the base
classes and implement only the abstract hooks.
- **Why "always these three packages"**: hand-rolling an iteration engine or progress channel
reintroduces known failures — publish/subscribe channel mismatch, no retry, untestable code. The
packages prevent these by design (e.g. `AgentTopic` structurally matches the publish and subscribe
channels).
- **Everything is an instance method, overridable via `extends`**. No pure functions or static-only
classes. Why: even if the library has a bug or a gap, the app can always work around it by extending
and overriding the relevant method (there is no true `final`). Write the app's overrides in the same
style.
## Cross-cutting rules
- **Business dependencies and transport are always received through `context`.** Do not directly
import AI clients, DB, HTTP, or queues inside an action or loop ([core.md](./references/core.md)).
Why: a direct import breaks in-process execution and stub injection, collapsing the one-way
dependency of the grand principle.
- **Return `null` / an empty array for missing values, not `undefined`.** **One property per line in
object literals** (applies to sample code too). **Avoid single-character variables and
`for` / `forEach` / `switch` / `else if`**; write with `map` / `filter` / `reduce` and early return.
Why: match the existing codebase style so review attention goes to the logic.
- **One class per file, one `export default`.** The constructor only assigns; defaults live in the
factory (`static create`); getters just return the field.
- **Comments in produced artifacts (`.js`) are English**, and this skill is written entirely in
English (prose, tables, and example-code comments).
## Choosing modules (what to install)
Stack adapters onto core per the requirement. **Core is always required**; add an adapter only when
that integration is needed.
| What you want | Packages needed |
| --- | --- |
| Run a loop in-process (CLI / tests / synchronous work / Lambda) | core only |
| Turn a long loop into a Redis job (horizontal scale, retry, rate limiting) | core + renchan-job |
| Launch over GraphQL and subscribe to progress in realtime | core + renchan-job + graphql |
- **Why stack incrementally**: actions and loops written with core alone can be turned into a job or a
GraphQL endpoint **without any change** (the adapter swaps only the transport step). Do not install
everything up front; layer on only what you need.
## End-to-end progress (the backbone of the realtime log)
"Launch with a mutation → return `accepted` immediately → stream progress over a subscription" is the
typical shape for an agent on the web. Progress follows this single path. Because the publish side
(the Worker) and the subscribe side (the Subscription) use the **same `AgentTopic`**, the channel
names match structurally.
```
[core] loop.run({ input, onProgress })
└ iterate emits emitProgress → onProgress(event) each iteration
[renchan-job] BaseAgentJobWorker: onProgress → job.updateProgress(event)
→ onJobProgress → publish to the channel AgentTopic.create({ channel, scope }).value
[graphql] BaseAgentProgressSubscriptionResolver: subscribes to the same channel + scope
[frontend] receives it in realtime over a GraphQL Subscription
```
- **Why use `AgentTopic` on both sides**: if the publish and subscribe channel names drift apart, you
get a silent "progress never arrives" failure. Concentrating the naming in one core class, with both
sides composing from the same `channel` + `scope`, makes drift impossible by design. To change the
naming rule, extend `AgentTopic` and override `get value()` in one place.
## Detail files
- [core.md](./references/core.md) — the core (`@openreachtech/mentsu-agent-loop-core`) spec and use
cases: actions / registry, the two loop types (AI-driven / procedural), composition (parallel
Composite / sequential Pipeline / ActionStage), Runners, `AgentTopic` / `AgentProgressEvent`, the
state model (state / context / compression).
- [graphql-integration.md](./references/graphql-integration.md) — the pattern for integrating with
renchan's GraphQL: the launch mutation (`BaseRequestAgentMutationResolver`) and the progress
subscription (`BaseAgentProgressSubscriptionResolver`), channel matching via `AgentTopic`.
- [renchan-job-integration.md](./references/renchan-job-integration.md) — the pattern for integrating
with renchan-job-bullmq: the Worker (`BaseAgentJobWorker`) / Dispatcher (`BaseAgentJobDispatcher`) /
`JobAgentRunner`, Engine / Share / Context wiring, `concurrency` / `limiter`, and the full
progress-publish flow.
- [testing.md](./references/testing.md) — testing guidelines: the contract test kit
(`LoopContractAssertion` / `ActionContractAssertion`) and the unit-test approach for actions / loops
/ composition / Worker / Dispatcher / Resolver (stub `context`, assert `onProgress`, assert abstract
throws).