@agentproto/agent-runtime 0.1.0-alpha.0
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/ARCHITECTURE.md +301 -0
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/adapters/dispatcher-mention.d.ts +38 -0
- package/dist/adapters/dispatcher-mention.mjs +45 -0
- package/dist/adapters/dispatcher-mention.mjs.map +1 -0
- package/dist/adapters/participant-agent-cli.d.ts +32 -0
- package/dist/adapters/participant-agent-cli.mjs +72 -0
- package/dist/adapters/participant-agent-cli.mjs.map +1 -0
- package/dist/adapters/state-fs.d.ts +25 -0
- package/dist/adapters/state-fs.mjs +54 -0
- package/dist/adapters/state-fs.mjs.map +1 -0
- package/dist/adapters/substrate-file.d.ts +39 -0
- package/dist/adapters/substrate-file.mjs +113 -0
- package/dist/adapters/substrate-file.mjs.map +1 -0
- package/dist/adapters/telemetry.d.ts +39 -0
- package/dist/adapters/telemetry.mjs +76 -0
- package/dist/adapters/telemetry.mjs.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.mjs +225 -0
- package/dist/index.mjs.map +1 -0
- package/dist/manifest.d.ts +56 -0
- package/dist/manifest.mjs +61 -0
- package/dist/manifest.mjs.map +1 -0
- package/dist/ports.d.ts +165 -0
- package/dist/ports.mjs +3 -0
- package/dist/ports.mjs.map +1 -0
- package/dist/util/mention-parser.d.mts +8 -0
- package/dist/util/mention-parser.mjs +33 -0
- package/package.json +108 -0
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# Architecture — @agentproto/agent-runtime
|
|
2
|
+
|
|
3
|
+
A small port/adapter kernel for running swarms of agents over swappable
|
|
4
|
+
conversation substrates. The kernel is transport-agnostic — it knows
|
|
5
|
+
nothing about specific chat servers, dispatchers, or executors; it only
|
|
6
|
+
routes through port interfaces.
|
|
7
|
+
|
|
8
|
+
## The one cycle
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
┌───────────────────┐
|
|
12
|
+
│ substrate.read │ 1. snapshot recent turns
|
|
13
|
+
└─────────┬─────────┘
|
|
14
|
+
│
|
|
15
|
+
▼
|
|
16
|
+
┌───────────────────────────┐
|
|
17
|
+
│ dispatcher.selectNext() │ 2. decide who speaks next
|
|
18
|
+
└─────────────┬─────────────┘
|
|
19
|
+
│ participantIds[]
|
|
20
|
+
┌───────────┴───────────┐
|
|
21
|
+
│ │
|
|
22
|
+
▼ ▼
|
|
23
|
+
┌──────────────────┐ ┌──────────────────┐
|
|
24
|
+
│ state.read(p) │ │ executor.execute │ 3. produce reply
|
|
25
|
+
└────────┬─────────┘ │ Turn │
|
|
26
|
+
│ └────────┬─────────┘
|
|
27
|
+
└──────────► input ─────┘
|
|
28
|
+
│
|
|
29
|
+
▼
|
|
30
|
+
┌────────────────────────┐
|
|
31
|
+
│ substrate.append() │ 4. write reply turn
|
|
32
|
+
└────────────┬───────────┘
|
|
33
|
+
│
|
|
34
|
+
▼
|
|
35
|
+
┌─────────────────────┐
|
|
36
|
+
│ state.write(p) │ 5. persist state diff
|
|
37
|
+
└──────────┬──────────┘
|
|
38
|
+
│
|
|
39
|
+
▼
|
|
40
|
+
┌────────────────────────┐
|
|
41
|
+
│ lifecycle.onTurnEnd │ 6. fire hooks
|
|
42
|
+
└────────────────────────┘
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`runTurn(ports, opts)` executes one cycle. Callers loop it for
|
|
46
|
+
continuous operation. The kernel has no concept of "running" — it's
|
|
47
|
+
strictly synchronous-per-cycle. Long-running behavior lives in the
|
|
48
|
+
caller (the `agentproto run-swarm` verb wraps it in a `do-while`).
|
|
49
|
+
|
|
50
|
+
## Ports
|
|
51
|
+
|
|
52
|
+
### `Substrate`
|
|
53
|
+
|
|
54
|
+
The conversation store. Append-only, oldest-first read semantics.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
interface Substrate {
|
|
58
|
+
readonly kind: string
|
|
59
|
+
append(turn: TurnInput): Promise<Turn>
|
|
60
|
+
read(since?: TurnId): Promise<readonly Turn[]>
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Reference adapter: `FileSubstrate` — a markdown journal at a path. Each
|
|
65
|
+
turn is delimited by `=== TURN id=… participant=… ts=… ===`.
|
|
66
|
+
|
|
67
|
+
Other implementations: chat servers (Slack, Discord), MCP-bridged
|
|
68
|
+
threads, REST APIs. Ship as plugin packages.
|
|
69
|
+
|
|
70
|
+
**`since` contract:** if `since` is provided but not in the window the
|
|
71
|
+
adapter fetched, the adapter SHOULD throw (telling the caller to raise
|
|
72
|
+
its fetch window) rather than silently returning everything — otherwise
|
|
73
|
+
the dispatcher's cursor can re-fire on already-handled turns.
|
|
74
|
+
|
|
75
|
+
### `Dispatcher`
|
|
76
|
+
|
|
77
|
+
Decides which participants speak next, given the recent turns and the
|
|
78
|
+
participant roster.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
interface Dispatcher {
|
|
82
|
+
readonly kind: string
|
|
83
|
+
selectNext(input: DispatcherInput): Promise<readonly ParticipantId[]>
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Reference adapter: `MentionDispatcher` — selects participants whose
|
|
88
|
+
`displayName` is `@-mentioned` in the most recent turn. Tracks an
|
|
89
|
+
in-memory cursor so it doesn't re-fire on the same trigger across
|
|
90
|
+
cycles.
|
|
91
|
+
|
|
92
|
+
Other implementations: round-robin, topic-routed, LLM-decided. The
|
|
93
|
+
dispatcher is the pluggable "who speaks next?" decision.
|
|
94
|
+
|
|
95
|
+
### `ParticipantExecutor`
|
|
96
|
+
|
|
97
|
+
Produces a turn for a given participant when the dispatcher selects
|
|
98
|
+
them.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
interface ParticipantExecutor {
|
|
102
|
+
readonly kind: string
|
|
103
|
+
executeTurn(input: ParticipantExecuteInput): Promise<ParticipantExecuteOutput>
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Reference adapter: `AgentCliParticipant` — spawns a CLI binary (`claude
|
|
108
|
+
--print`, `hermes -p`, etc.), pipes the assembled prompt over stdin,
|
|
109
|
+
returns parsed stdout as the turn body.
|
|
110
|
+
|
|
111
|
+
Other implementations: in-process LLM SDK calls, server-side delegation
|
|
112
|
+
(e.g. MCP `run_operator`), webhook-driven external workers.
|
|
113
|
+
|
|
114
|
+
Each participant in the manifest declares an `executor` kind; the
|
|
115
|
+
runtime keeps one executor instance per kind, shared across
|
|
116
|
+
participants of that kind.
|
|
117
|
+
|
|
118
|
+
### `StateStore`
|
|
119
|
+
|
|
120
|
+
Per-participant scratch state, persisted across turns.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
interface StateStore {
|
|
124
|
+
readonly kind: string
|
|
125
|
+
read(participantId: ParticipantId): Promise<Readonly<Record<string, unknown>>>
|
|
126
|
+
write(participantId: ParticipantId, state: Readonly<Record<string, unknown>>): Promise<void>
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Reference adapter: `FileStateStore` — one JSON file per participant.
|
|
131
|
+
|
|
132
|
+
The state is what the executor returns in `stateUpdate`; the kernel
|
|
133
|
+
writes it after the substrate append. Useful for executors that
|
|
134
|
+
maintain working memory between turns (counters, scratchpads,
|
|
135
|
+
operator-context summaries).
|
|
136
|
+
|
|
137
|
+
### `Lifecycle` (optional)
|
|
138
|
+
|
|
139
|
+
Sparse hooks fired at the edges of a cycle. Adapters opt in by
|
|
140
|
+
implementing some/all callbacks.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
interface Lifecycle {
|
|
144
|
+
onTurnEnd?(turn: Turn): Promise<void> | void
|
|
145
|
+
onMention?(target: ParticipantId, byTurn: Turn): Promise<void> | void
|
|
146
|
+
onIdle?(): Promise<void> | void
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Useful for: domain hooks where the caller needs to react to a turn
|
|
151
|
+
(e.g. mirror it elsewhere). For structured observability of every
|
|
152
|
+
phase boundary, use the `Telemetry` port below instead.
|
|
153
|
+
|
|
154
|
+
### `Telemetry` (optional)
|
|
155
|
+
|
|
156
|
+
A single sink for structured per-phase events:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
interface Telemetry {
|
|
160
|
+
emit(event: TelemetryEvent): void
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Every event carries `cycleId` + `at`; sinks group on cycleId to
|
|
165
|
+
rebuild OTEL-style spans. Event kinds emitted per cycle:
|
|
166
|
+
`cycle.started`, `substrate.read`, `dispatch.decided`,
|
|
167
|
+
`participant.started`/`finished`/`failed`, `substrate.appended`,
|
|
168
|
+
`state.written`, `cycle.idle`, `cycle.finished`.
|
|
169
|
+
|
|
170
|
+
Reference adapters in
|
|
171
|
+
`@agentproto/agent-runtime/adapters/telemetry`:
|
|
172
|
+
|
|
173
|
+
- `noopTelemetry` — silent default
|
|
174
|
+
- `stderrTelemetry({ prefix?, include?, exclude? })` — one human-readable line per event
|
|
175
|
+
- `arrayTelemetry()` — in-memory, for tests
|
|
176
|
+
- `composeTelemetry(...sinks)` — fan-out
|
|
177
|
+
|
|
178
|
+
Wire your own (OTEL exporter, JSON-lines log, metrics counter, …) by
|
|
179
|
+
implementing the interface. Sinks that throw are isolated — kernel
|
|
180
|
+
never crashes a cycle on a telemetry failure.
|
|
181
|
+
|
|
182
|
+
## Composition
|
|
183
|
+
|
|
184
|
+
`RuntimePorts` is the tuple the kernel runs against:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
type RuntimePorts = {
|
|
188
|
+
substrate: Substrate
|
|
189
|
+
dispatcher: Dispatcher
|
|
190
|
+
state: StateStore
|
|
191
|
+
lifecycle?: Lifecycle
|
|
192
|
+
participants: readonly ParticipantDescriptor[]
|
|
193
|
+
executors: ReadonlyMap<string, ParticipantExecutor>
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Built by the caller. The reference path is `@agentproto/cli`'s
|
|
198
|
+
`run-swarm` verb — it reads a manifest, looks up each `kind` in the
|
|
199
|
+
runtime registry (`@agentproto/cli/registry/runtime`), and builds the
|
|
200
|
+
ports tuple. The kernel itself never matches `kind` strings; that
|
|
201
|
+
happens in the wiring layer.
|
|
202
|
+
|
|
203
|
+
## Manifest
|
|
204
|
+
|
|
205
|
+
Markdown with YAML frontmatter (the same doctype convention
|
|
206
|
+
`@agentproto` uses elsewhere):
|
|
207
|
+
|
|
208
|
+
```yaml
|
|
209
|
+
---
|
|
210
|
+
schema: agentruntimes/v1
|
|
211
|
+
kind: MultiAgentRuntime
|
|
212
|
+
id: my-swarm
|
|
213
|
+
participants:
|
|
214
|
+
- id: reviewer
|
|
215
|
+
executor: agent-cli
|
|
216
|
+
displayName: Reviewer
|
|
217
|
+
role: ../.claude/agents/reviewer.md
|
|
218
|
+
substrate:
|
|
219
|
+
kind: file
|
|
220
|
+
path: ./conversation.md
|
|
221
|
+
dispatcher:
|
|
222
|
+
kind: mention
|
|
223
|
+
state:
|
|
224
|
+
kind: fs
|
|
225
|
+
dir: ./state
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
Free-form documentation of what this swarm does.
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Each adapter block (`substrate`, `dispatcher`, `state`) carries a
|
|
232
|
+
`kind` plus arbitrary host-extension fields. The validating zod schema
|
|
233
|
+
(`manifest.ts`) uses `.loose()` on the adapter blocks — the kind's
|
|
234
|
+
factory reads its own typed fields off the block at build time.
|
|
235
|
+
|
|
236
|
+
## Extension points
|
|
237
|
+
|
|
238
|
+
### Adding a new substrate / dispatcher / executor / state store
|
|
239
|
+
|
|
240
|
+
Implement the relevant port interface. Then register through
|
|
241
|
+
`@agentproto/cli/registry/runtime`:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
// in your-plugin/src/index.ts
|
|
245
|
+
import { registerSubstrate } from "@agentproto/cli/registry/runtime"
|
|
246
|
+
import { MySubstrate } from "./my-substrate.js"
|
|
247
|
+
|
|
248
|
+
registerSubstrate("my-kind", (cfg, ctx) => {
|
|
249
|
+
return new MySubstrate({
|
|
250
|
+
foo: typeof cfg.foo === "string" ? cfg.foo : "default",
|
|
251
|
+
// …
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Users wire it via `--plugin <your-package>` on `agentproto run-swarm`,
|
|
257
|
+
or by listing it under `plugins[]` in `~/.agentproto/config.json`.
|
|
258
|
+
|
|
259
|
+
### Adapter context
|
|
260
|
+
|
|
261
|
+
Each factory receives an `AdapterContext`:
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
interface AdapterContext {
|
|
265
|
+
readonly baseDir: string // manifest's dir
|
|
266
|
+
registerCleanup(fn: () => Promise<void> | void): void
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
`registerCleanup` is the place to register teardown for adapters that
|
|
271
|
+
hold disposable resources (MCP clients, sockets, child processes). The
|
|
272
|
+
CLI calls every registered callback in order when the swarm shuts down.
|
|
273
|
+
|
|
274
|
+
## Invariants
|
|
275
|
+
|
|
276
|
+
- **The kernel never matches `kind` strings.** It dispatches off the
|
|
277
|
+
port interface (`substrate.append()`, `executor.executeTurn()`, etc.)
|
|
278
|
+
and looks executors up in the `executors` map. Kind resolution
|
|
279
|
+
happens in the wiring layer (the CLI), never in `runtime.ts`.
|
|
280
|
+
- **A single cycle is synchronous-per-step.** Substrate append, state
|
|
281
|
+
write, lifecycle hooks run sequentially within a cycle. Concurrency
|
|
282
|
+
across multiple selected participants is intentionally **not**
|
|
283
|
+
provided by the kernel — adapters can parallelise internally if they
|
|
284
|
+
want, but the contract is one-by-one.
|
|
285
|
+
- **Failure recovery is the caller's job.** `runTurn` propagates
|
|
286
|
+
exceptions. The CLI's run-loop catches them and continues; a library
|
|
287
|
+
user is free to do anything else.
|
|
288
|
+
|
|
289
|
+
## What is intentionally NOT in scope
|
|
290
|
+
|
|
291
|
+
- **Tool / effector binding.** The kernel doesn't model "this
|
|
292
|
+
participant has these tools available." Executors handle that
|
|
293
|
+
themselves — the agent-cli executor passes whatever flags the CLI
|
|
294
|
+
needs; an in-process executor wires tools directly. If structured
|
|
295
|
+
binding becomes useful later, it'd go in `ParticipantDescriptor.meta`
|
|
296
|
+
or via a future `Effector` port.
|
|
297
|
+
- **Routing across substrates / federation.** One swarm = one
|
|
298
|
+
substrate. Federation (one operator participating in many
|
|
299
|
+
conversations) is the caller's composition concern.
|
|
300
|
+
- **Persistence schemas beyond `StateStore`.** Auditing, tracing,
|
|
301
|
+
evaluation runs — all live outside the kernel.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 agentproto contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @agentproto/agent-runtime
|
|
2
|
+
|
|
3
|
+
MultiAgentRuntime kernel — a small port/adapter framework for running
|
|
4
|
+
swarms of agents over swappable conversation substrates.
|
|
5
|
+
|
|
6
|
+
**Status:** alpha.
|
|
7
|
+
|
|
8
|
+
## What this is
|
|
9
|
+
|
|
10
|
+
A composition primitive, not a product. The kernel knows nothing
|
|
11
|
+
about specific transports, dispatchers, or executors — it only routes
|
|
12
|
+
through port interfaces. Mode (local file journal, hosted chat,
|
|
13
|
+
MCP-bridged thread, …) is decided entirely by the adapters the caller
|
|
14
|
+
wires in.
|
|
15
|
+
|
|
16
|
+
## Ports
|
|
17
|
+
|
|
18
|
+
| Port | What it does |
|
|
19
|
+
| --------------------- | ------------------------------------------------------------------- |
|
|
20
|
+
| `Substrate` | Append-only conversation store. `append(turn)` + `read(since?)`. |
|
|
21
|
+
| `Dispatcher` | Decides which participants speak next given recent turns. |
|
|
22
|
+
| `ParticipantExecutor` | Produces a turn for a given participant on demand. |
|
|
23
|
+
| `StateStore` | Per-participant scratch state across turns. |
|
|
24
|
+
| `Lifecycle` | Optional hooks: `onTurnEnd`, `onMention`, `onIdle`. |
|
|
25
|
+
| `EffectorBinding` | Optional per-participant tool / MCP bindings (consumer-supplied). |
|
|
26
|
+
|
|
27
|
+
A `RuntimePorts` is a tuple of these. `runTurn(ports, options)` runs
|
|
28
|
+
one dispatcher cycle:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
read substrate → dispatch → execute selected participants → append → fire lifecycle
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Loop it for continuous operation.
|
|
35
|
+
|
|
36
|
+
## Reference adapters
|
|
37
|
+
|
|
38
|
+
Shipped in this package under `@agentproto/agent-runtime/adapters/*`:
|
|
39
|
+
|
|
40
|
+
| Adapter | Kind | Notes |
|
|
41
|
+
| ---------------------------------------------------- | ------------ | -------------------------------------------------- |
|
|
42
|
+
| `substrate-file` | `file` | Append-only markdown journal at a given path. |
|
|
43
|
+
| `dispatcher-mention` | `mention` | Selects participants @-mentioned in the trigger. |
|
|
44
|
+
| `state-fs` | `fs` | One JSON file per participant. |
|
|
45
|
+
| `participant-agent-cli` | `agent-cli` | Spawns a CLI binary (`claude --print`, etc.). |
|
|
46
|
+
|
|
47
|
+
## Manifest
|
|
48
|
+
|
|
49
|
+
A `MultiAgentRuntime` manifest is markdown with YAML frontmatter:
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
---
|
|
53
|
+
schema: agentruntimes/v1
|
|
54
|
+
kind: MultiAgentRuntime
|
|
55
|
+
id: my-swarm
|
|
56
|
+
participants:
|
|
57
|
+
- id: reviewer
|
|
58
|
+
executor: agent-cli
|
|
59
|
+
displayName: Reviewer
|
|
60
|
+
role: ../.claude/agents/reviewer.md
|
|
61
|
+
substrate:
|
|
62
|
+
kind: file
|
|
63
|
+
path: ./conversation.md
|
|
64
|
+
dispatcher:
|
|
65
|
+
kind: mention
|
|
66
|
+
state:
|
|
67
|
+
kind: fs
|
|
68
|
+
dir: ./state
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
Free-form documentation of what this swarm does.
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`@agentproto/cli`'s `run-swarm` verb loads such manifests, resolves
|
|
75
|
+
each `kind` string through its adapter registry, and runs cycles in a
|
|
76
|
+
loop. Third-party adapters register through `@agentproto/cli/registry/runtime`.
|
|
77
|
+
|
|
78
|
+
## Extending
|
|
79
|
+
|
|
80
|
+
Implement any of the ports and ship it as your own package. To plug
|
|
81
|
+
into the CLI, export a module that calls `registerSubstrate` /
|
|
82
|
+
`registerDispatcher` / `registerExecutor` from
|
|
83
|
+
`@agentproto/cli/registry/runtime` at load time, then point users at
|
|
84
|
+
it with `--plugin <your-module-id>` or via `~/.agentproto/config.json`.
|
|
85
|
+
|
|
86
|
+
See [ARCHITECTURE.md](./ARCHITECTURE.md) for the port-by-port walk,
|
|
87
|
+
cycle diagram, and invariants.
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Dispatcher, DispatcherInput, ParticipantId } from '../ports.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type sidecar for `mention-parser.mjs`. The implementation file is
|
|
5
|
+
* vanilla JavaScript so runtime profiles can stamp its source into
|
|
6
|
+
* Claude Code hooks at build time; this `.d.mts` keeps the TS-side
|
|
7
|
+
* type contract crisp.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
declare function textContainsMention(text: string, name: string): boolean
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Mention dispatcher — selects participants whose displayName is
|
|
14
|
+
* @-mentioned in the most recent turn.
|
|
15
|
+
*
|
|
16
|
+
* Skip rules:
|
|
17
|
+
* - never re-select the participant who authored the trigger turn
|
|
18
|
+
* (otherwise they'd reply to themselves indefinitely)
|
|
19
|
+
* - never dispatch on a trigger we already processed (in-memory cursor —
|
|
20
|
+
* prevents the loop from re-firing on the same @mention every poll
|
|
21
|
+
* when the kernel's own reply isn't yet visible in the next read).
|
|
22
|
+
* Per-dispatcher-instance state; a fresh swarm process starts with
|
|
23
|
+
* no cursor and naturally responds to the latest unhandled mention
|
|
24
|
+
* exactly once.
|
|
25
|
+
* - if no mentions match any known participant, return [] (idle)
|
|
26
|
+
*
|
|
27
|
+
* The parser itself lives in `../util/mention-parser.mjs` (vanilla JS)
|
|
28
|
+
* so runtime profiles can stamp the same implementation into Claude
|
|
29
|
+
* Code hooks at build time without depending on the TS build pipeline.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
declare class MentionDispatcher implements Dispatcher {
|
|
33
|
+
readonly kind = "mention";
|
|
34
|
+
private lastProcessedTriggerId;
|
|
35
|
+
selectNext(input: DispatcherInput): Promise<readonly ParticipantId[]>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { MentionDispatcher, textContainsMention };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @agentproto/agent-runtime v0.1.0-alpha
|
|
3
|
+
* MultiAgentRuntime kernel — swappable ports + reference adapters
|
|
4
|
+
* (file substrate, mention dispatcher, fs state, agent-cli participant).
|
|
5
|
+
* Transport-specific adapters ship in separate packages.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// src/util/mention-parser.mjs
|
|
9
|
+
function textContainsMention(text, name) {
|
|
10
|
+
if (text.includes(`@${name}`)) return true;
|
|
11
|
+
if (name.includes(" ")) {
|
|
12
|
+
const firstName = name.split(" ")[0]?.trim();
|
|
13
|
+
if (!firstName) return false;
|
|
14
|
+
const regex = new RegExp(`@${firstName}(?!\\w)`, "i");
|
|
15
|
+
return regex.test(text);
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/adapters/dispatcher-mention.ts
|
|
21
|
+
var MentionDispatcher = class {
|
|
22
|
+
kind = "mention";
|
|
23
|
+
lastProcessedTriggerId;
|
|
24
|
+
async selectNext(input) {
|
|
25
|
+
const trigger = input.recentTurns[input.recentTurns.length - 1];
|
|
26
|
+
if (!trigger) return [];
|
|
27
|
+
if (trigger.id === this.lastProcessedTriggerId) return [];
|
|
28
|
+
const text = trigger.content;
|
|
29
|
+
const selected = [];
|
|
30
|
+
for (const p of input.participants) {
|
|
31
|
+
if (p.id === trigger.participantId) continue;
|
|
32
|
+
if (textContainsMention(text, p.displayName)) {
|
|
33
|
+
selected.push(p.id);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (selected.length > 0) {
|
|
37
|
+
this.lastProcessedTriggerId = trigger.id;
|
|
38
|
+
}
|
|
39
|
+
return selected;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export { MentionDispatcher, textContainsMention };
|
|
44
|
+
//# sourceMappingURL=dispatcher-mention.mjs.map
|
|
45
|
+
//# sourceMappingURL=dispatcher-mention.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/util/mention-parser.mjs","../../src/adapters/dispatcher-mention.ts"],"names":[],"mappings":";;;;;;;;AAuBO,SAAS,mBAAA,CAAoB,MAAM,IAAA,EAAM;AAC9C,EAAA,IAAI,KAAK,QAAA,CAAS,CAAA,CAAA,EAAI,IAAI,CAAA,CAAE,GAAG,OAAO,IAAA;AACtC,EAAA,IAAI,IAAA,CAAK,QAAA,CAAS,GAAG,CAAA,EAAG;AACtB,IAAA,MAAM,YAAY,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC,GAAG,IAAA,EAAK;AAC3C,IAAA,IAAI,CAAC,WAAW,OAAO,KAAA;AACvB,IAAA,MAAM,QAAQ,IAAI,MAAA,CAAO,CAAA,CAAA,EAAI,SAAS,WAAW,GAAG,CAAA;AACpD,IAAA,OAAO,KAAA,CAAM,KAAK,IAAI,CAAA;AAAA,EACxB;AACA,EAAA,OAAO,KAAA;AACT;;;ACJO,IAAM,oBAAN,MAA8C;AAAA,EAC1C,IAAA,GAAO,SAAA;AAAA,EACR,sBAAA;AAAA,EAER,MAAM,WAAW,KAAA,EAA2D;AAC1E,IAAA,MAAM,UAAU,KAAA,CAAM,WAAA,CAAY,KAAA,CAAM,WAAA,CAAY,SAAS,CAAC,CAAA;AAC9D,IAAA,IAAI,CAAC,OAAA,EAAS,OAAO,EAAC;AACtB,IAAA,IAAI,OAAA,CAAQ,EAAA,KAAO,IAAA,CAAK,sBAAA,SAA+B,EAAC;AACxD,IAAA,MAAM,OAAO,OAAA,CAAQ,OAAA;AAErB,IAAA,MAAM,WAA4B,EAAC;AACnC,IAAA,KAAA,MAAW,CAAA,IAAK,MAAM,YAAA,EAAc;AAClC,MAAA,IAAI,CAAA,CAAE,EAAA,KAAO,OAAA,CAAQ,aAAA,EAAe;AACpC,MAAA,IAAI,mBAAA,CAAoB,IAAA,EAAM,CAAA,CAAE,WAAW,CAAA,EAAG;AAC5C,QAAA,QAAA,CAAS,IAAA,CAAK,EAAE,EAAE,CAAA;AAAA,MACpB;AAAA,IACF;AAEA,IAAA,IAAI,QAAA,CAAS,SAAS,CAAA,EAAG;AACvB,MAAA,IAAA,CAAK,yBAAyB,OAAA,CAAQ,EAAA;AAAA,IACxC;AACA,IAAA,OAAO,QAAA;AAAA,EACT;AACF","file":"dispatcher-mention.mjs","sourcesContent":["/**\n * Canonical @-mention parser.\n *\n * Lives as raw JavaScript (no types) because it's consumed in two\n * places that don't share a build:\n *\n * 1. The kernel imports `textContainsMention` from this file as a\n * normal ESM module (the dispatcher in `adapters/dispatcher-mention.ts`).\n *\n * 2. Runtime profiles inline this file's source into Claude Code\n * hooks at profile-build time, because `.claude/` has no\n * module-resolution at hook-execution time. The build script\n * reads this file as text and stamps it into the hook template.\n *\n * Two cases:\n * 1. Literal `@<name>` substring (case-sensitive, full name).\n * 2. For multi-word names, also `@<firstName>` with non-word\n * boundary (case-insensitive).\n *\n * Edit here only — both consumers stay in sync via build pipelines.\n */\n\n/** @param {string} text @param {string} name @returns {boolean} */\nexport function textContainsMention(text, name) {\n if (text.includes(`@${name}`)) return true\n if (name.includes(\" \")) {\n const firstName = name.split(\" \")[0]?.trim()\n if (!firstName) return false\n const regex = new RegExp(`@${firstName}(?!\\\\w)`, \"i\")\n return regex.test(text)\n }\n return false\n}\n","/**\n * Mention dispatcher — selects participants whose displayName is\n * @-mentioned in the most recent turn.\n *\n * Skip rules:\n * - never re-select the participant who authored the trigger turn\n * (otherwise they'd reply to themselves indefinitely)\n * - never dispatch on a trigger we already processed (in-memory cursor —\n * prevents the loop from re-firing on the same @mention every poll\n * when the kernel's own reply isn't yet visible in the next read).\n * Per-dispatcher-instance state; a fresh swarm process starts with\n * no cursor and naturally responds to the latest unhandled mention\n * exactly once.\n * - if no mentions match any known participant, return [] (idle)\n *\n * The parser itself lives in `../util/mention-parser.mjs` (vanilla JS)\n * so runtime profiles can stamp the same implementation into Claude\n * Code hooks at build time without depending on the TS build pipeline.\n */\n\nimport type {\n Dispatcher,\n DispatcherInput,\n ParticipantId,\n TurnId,\n} from \"../ports.js\"\nimport { textContainsMention } from \"../util/mention-parser.mjs\"\n\nexport class MentionDispatcher implements Dispatcher {\n readonly kind = \"mention\"\n private lastProcessedTriggerId: TurnId | undefined\n\n async selectNext(input: DispatcherInput): Promise<readonly ParticipantId[]> {\n const trigger = input.recentTurns[input.recentTurns.length - 1]\n if (!trigger) return []\n if (trigger.id === this.lastProcessedTriggerId) return []\n const text = trigger.content\n\n const selected: ParticipantId[] = []\n for (const p of input.participants) {\n if (p.id === trigger.participantId) continue\n if (textContainsMention(text, p.displayName)) {\n selected.push(p.id)\n }\n }\n\n if (selected.length > 0) {\n this.lastProcessedTriggerId = trigger.id\n }\n return selected\n }\n}\n\n// Re-exported so callers writing their own adapters can detect\n// mentions with identical semantics.\nexport { textContainsMention }\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ParticipantExecutor, ParticipantExecuteInput, ParticipantExecuteOutput } from '../ports.js';
|
|
2
|
+
export { parseClaudeJsonOutput } from '@agentproto/cli-exec';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Agent-CLI participant — spawns a CLI binary, pipes the assembled prompt
|
|
6
|
+
* over stdin, and uses captured stdout as the turn content.
|
|
7
|
+
*
|
|
8
|
+
* Works for any CLI that supports a one-shot "read prompt from stdin,
|
|
9
|
+
* print response to stdout" mode (claude-code's `--print`, hermes' `-p`,
|
|
10
|
+
* etc.).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
type AgentCliParticipantOptions = {
|
|
14
|
+
/** Executable to invoke. Examples: "claude", "hermes", "goose". */
|
|
15
|
+
readonly command: string;
|
|
16
|
+
/** Static args. The prompt is fed over stdin, not as an argument. */
|
|
17
|
+
readonly args?: readonly string[];
|
|
18
|
+
/** Working directory. Defaults to process.cwd(). */
|
|
19
|
+
readonly cwd?: string;
|
|
20
|
+
/** Hard timeout in ms. Default 90000. */
|
|
21
|
+
readonly timeoutMs?: number;
|
|
22
|
+
/** Optional output parser — if returns null, content falls back to raw stdout. */
|
|
23
|
+
readonly parseOutput?: (stdout: string) => string | null;
|
|
24
|
+
};
|
|
25
|
+
declare class AgentCliParticipant implements ParticipantExecutor {
|
|
26
|
+
private readonly opts;
|
|
27
|
+
readonly kind = "agent-cli";
|
|
28
|
+
constructor(opts: AgentCliParticipantOptions);
|
|
29
|
+
executeTurn(input: ParticipantExecuteInput): Promise<ParticipantExecuteOutput>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { AgentCliParticipant, type AgentCliParticipantOptions };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import matter from 'gray-matter';
|
|
3
|
+
import { spawnWithStdin } from '@agentproto/cli-exec';
|
|
4
|
+
export { parseClaudeJsonOutput } from '@agentproto/cli-exec';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @agentproto/agent-runtime v0.1.0-alpha
|
|
8
|
+
* MultiAgentRuntime kernel — swappable ports + reference adapters
|
|
9
|
+
* (file substrate, mention dispatcher, fs state, agent-cli participant).
|
|
10
|
+
* Transport-specific adapters ship in separate packages.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
var AgentCliParticipant = class {
|
|
14
|
+
constructor(opts) {
|
|
15
|
+
this.opts = opts;
|
|
16
|
+
}
|
|
17
|
+
opts;
|
|
18
|
+
kind = "agent-cli";
|
|
19
|
+
async executeTurn(input) {
|
|
20
|
+
const prompt = await assemblePrompt(input);
|
|
21
|
+
const stdout = await spawnWithStdin({
|
|
22
|
+
command: this.opts.command,
|
|
23
|
+
args: this.opts.args ?? [],
|
|
24
|
+
cwd: this.opts.cwd ?? process.cwd(),
|
|
25
|
+
timeoutMs: this.opts.timeoutMs ?? 9e4,
|
|
26
|
+
stdin: prompt,
|
|
27
|
+
signal: input.signal
|
|
28
|
+
});
|
|
29
|
+
const parsed = this.opts.parseOutput?.(stdout) ?? null;
|
|
30
|
+
const content = parsed ?? stdout.trimEnd();
|
|
31
|
+
return { content };
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
async function assemblePrompt(input) {
|
|
35
|
+
const roleText = input.participant.role ? await loadRole(input.participant.role) : "";
|
|
36
|
+
const transcript = input.recentTurns.map((t) => `[${t.participantId}] ${t.content}`).join("\n\n");
|
|
37
|
+
return [
|
|
38
|
+
roleText && `# Your role
|
|
39
|
+
|
|
40
|
+
${roleText}`,
|
|
41
|
+
`# Recent conversation
|
|
42
|
+
|
|
43
|
+
${transcript}`,
|
|
44
|
+
`# Your turn
|
|
45
|
+
|
|
46
|
+
You are ${input.participant.displayName}. The latest message in the conversation triggered you (most likely because it mentions you). Read the transcript above and reply in character. Keep it conversational unless the trigger asks for detailed work. Output only your reply \u2014 no preamble, no role labels, no quotes around the response.`
|
|
47
|
+
].filter(Boolean).join("\n\n");
|
|
48
|
+
}
|
|
49
|
+
var ROLE_FILE_EXTENSIONS = [".md", ".markdown", ".txt"];
|
|
50
|
+
async function loadRole(roleField) {
|
|
51
|
+
if (!looksLikeRoleFile(roleField)) return roleField;
|
|
52
|
+
try {
|
|
53
|
+
const raw = await readFile(roleField, "utf8");
|
|
54
|
+
const parsed = matter(raw);
|
|
55
|
+
return parsed.content.trim();
|
|
56
|
+
} catch (err) {
|
|
57
|
+
const code = err.code;
|
|
58
|
+
process.stderr.write(
|
|
59
|
+
`agent-cli participant: role path '${roleField}' not readable (${code ?? "unknown"}); using the literal string as inline role text.
|
|
60
|
+
`
|
|
61
|
+
);
|
|
62
|
+
return roleField;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function looksLikeRoleFile(s) {
|
|
66
|
+
const lower = s.toLowerCase();
|
|
67
|
+
return ROLE_FILE_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export { AgentCliParticipant };
|
|
71
|
+
//# sourceMappingURL=participant-agent-cli.mjs.map
|
|
72
|
+
//# sourceMappingURL=participant-agent-cli.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/participant-agent-cli.ts"],"names":[],"mappings":";;;;;;;;;;;;AAmCO,IAAM,sBAAN,MAAyD;AAAA,EAG9D,YAA6B,IAAA,EAAkC;AAAlC,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAAA,EAAmC;AAAA,EAAnC,IAAA;AAAA,EAFpB,IAAA,GAAO,WAAA;AAAA,EAIhB,MAAM,YACJ,KAAA,EACmC;AACnC,IAAA,MAAM,MAAA,GAAS,MAAM,cAAA,CAAe,KAAK,CAAA;AACzC,IAAA,MAAM,MAAA,GAAS,MAAM,cAAA,CAAe;AAAA,MAClC,OAAA,EAAS,KAAK,IAAA,CAAK,OAAA;AAAA,MACnB,IAAA,EAAM,IAAA,CAAK,IAAA,CAAK,IAAA,IAAQ,EAAC;AAAA,MACzB,GAAA,EAAK,IAAA,CAAK,IAAA,CAAK,GAAA,IAAO,QAAQ,GAAA,EAAI;AAAA,MAClC,SAAA,EAAW,IAAA,CAAK,IAAA,CAAK,SAAA,IAAa,GAAA;AAAA,MAClC,KAAA,EAAO,MAAA;AAAA,MACP,QAAQ,KAAA,CAAM;AAAA,KACf,CAAA;AACD,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,IAAA,CAAK,WAAA,GAAc,MAAM,CAAA,IAAK,IAAA;AAClD,IAAA,MAAM,OAAA,GAAU,MAAA,IAAU,MAAA,CAAO,OAAA,EAAQ;AACzC,IAAA,OAAO,EAAE,OAAA,EAAQ;AAAA,EACnB;AACF;AAEA,eAAe,eACb,KAAA,EACiB;AACjB,EAAA,MAAM,QAAA,GAAW,MAAM,WAAA,CAAY,IAAA,GAC/B,MAAM,QAAA,CAAS,KAAA,CAAM,WAAA,CAAY,IAAI,CAAA,GACrC,EAAA;AAEJ,EAAA,MAAM,UAAA,GAAa,KAAA,CAAM,WAAA,CACtB,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAA,EAAI,CAAA,CAAE,aAAa,KAAK,CAAA,CAAE,OAAO,CAAA,CAAE,CAAA,CAC9C,KAAK,MAAM,CAAA;AAEd,EAAA,OAAO;AAAA,IACL,QAAA,IAAY,CAAA;;AAAA,EAAkB,QAAQ,CAAA,CAAA;AAAA,IACtC,CAAA;;AAAA,EAA4B,UAAU,CAAA,CAAA;AAAA,IACtC,CAAA;;AAAA,QAAA,EAA0B,KAAA,CAAM,YAAY,WAAW,CAAA,2SAAA;AAAA,GACzD,CACG,MAAA,CAAO,OAAO,CAAA,CACd,KAAK,MAAM,CAAA;AAChB;AAKA,IAAM,oBAAA,GAAuB,CAAC,KAAA,EAAO,WAAA,EAAa,MAAM,CAAA;AAExD,eAAe,SAAS,SAAA,EAAoC;AAC1D,EAAA,IAAI,CAAC,iBAAA,CAAkB,SAAS,CAAA,EAAG,OAAO,SAAA;AAC1C,EAAA,IAAI;AACF,IAAA,MAAM,GAAA,GAAM,MAAM,QAAA,CAAS,SAAA,EAAW,MAAM,CAAA;AAI5C,IAAA,MAAM,MAAA,GAAS,OAAO,GAAG,CAAA;AACzB,IAAA,OAAO,MAAA,CAAO,QAAQ,IAAA,EAAK;AAAA,EAC7B,SAAS,GAAA,EAAK;AAKZ,IAAA,MAAM,OAAQ,GAAA,CAA8B,IAAA;AAC5C,IAAA,OAAA,CAAQ,MAAA,CAAO,KAAA;AAAA,MACb,CAAA,kCAAA,EAAqC,SAAS,CAAA,gBAAA,EAAmB,IAAA,IAAQ,SAAS,CAAA;AAAA;AAAA,KACpF;AACA,IAAA,OAAO,SAAA;AAAA,EACT;AACF;AAEA,SAAS,kBAAkB,CAAA,EAAoB;AAC7C,EAAA,MAAM,KAAA,GAAQ,EAAE,WAAA,EAAY;AAC5B,EAAA,OAAO,qBAAqB,IAAA,CAAK,CAAC,QAAQ,KAAA,CAAM,QAAA,CAAS,GAAG,CAAC,CAAA;AAC/D","file":"participant-agent-cli.mjs","sourcesContent":["/**\n * Agent-CLI participant — spawns a CLI binary, pipes the assembled prompt\n * over stdin, and uses captured stdout as the turn content.\n *\n * Works for any CLI that supports a one-shot \"read prompt from stdin,\n * print response to stdout\" mode (claude-code's `--print`, hermes' `-p`,\n * etc.).\n */\n\nimport { readFile } from \"node:fs/promises\"\nimport matter from \"gray-matter\"\nimport { spawnWithStdin } from \"@agentproto/cli-exec\"\nimport type {\n ParticipantExecuteInput,\n ParticipantExecuteOutput,\n ParticipantExecutor,\n} from \"../ports.js\"\n\n// Re-exported for back-compat: the JSON-envelope parser now lives in the shared\n// @agentproto/cli-exec package alongside the spawn helper.\nexport { parseClaudeJsonOutput } from \"@agentproto/cli-exec\"\n\nexport type AgentCliParticipantOptions = {\n /** Executable to invoke. Examples: \"claude\", \"hermes\", \"goose\". */\n readonly command: string\n /** Static args. The prompt is fed over stdin, not as an argument. */\n readonly args?: readonly string[]\n /** Working directory. Defaults to process.cwd(). */\n readonly cwd?: string\n /** Hard timeout in ms. Default 90000. */\n readonly timeoutMs?: number\n /** Optional output parser — if returns null, content falls back to raw stdout. */\n readonly parseOutput?: (stdout: string) => string | null\n}\n\nexport class AgentCliParticipant implements ParticipantExecutor {\n readonly kind = \"agent-cli\"\n\n constructor(private readonly opts: AgentCliParticipantOptions) {}\n\n async executeTurn(\n input: ParticipantExecuteInput\n ): Promise<ParticipantExecuteOutput> {\n const prompt = await assemblePrompt(input)\n const stdout = await spawnWithStdin({\n command: this.opts.command,\n args: this.opts.args ?? [],\n cwd: this.opts.cwd ?? process.cwd(),\n timeoutMs: this.opts.timeoutMs ?? 90000,\n stdin: prompt,\n signal: input.signal,\n })\n const parsed = this.opts.parseOutput?.(stdout) ?? null\n const content = parsed ?? stdout.trimEnd()\n return { content }\n }\n}\n\nasync function assemblePrompt(\n input: ParticipantExecuteInput\n): Promise<string> {\n const roleText = input.participant.role\n ? await loadRole(input.participant.role)\n : \"\"\n\n const transcript = input.recentTurns\n .map((t) => `[${t.participantId}] ${t.content}`)\n .join(\"\\n\\n\")\n\n return [\n roleText && `# Your role\\n\\n${roleText}`,\n `# Recent conversation\\n\\n${transcript}`,\n `# Your turn\\n\\nYou are ${input.participant.displayName}. The latest message in the conversation triggered you (most likely because it mentions you). Read the transcript above and reply in character. Keep it conversational unless the trigger asks for detailed work. Output only your reply — no preamble, no role labels, no quotes around the response.`,\n ]\n .filter(Boolean)\n .join(\"\\n\\n\")\n}\n\n// File extensions we treat as a path-to-role-file. Anything else is\n// inline role text — even strings that contain `/`, which are common\n// in normal sentences (\"I am an AI/ML reviewer\").\nconst ROLE_FILE_EXTENSIONS = [\".md\", \".markdown\", \".txt\"]\n\nasync function loadRole(roleField: string): Promise<string> {\n if (!looksLikeRoleFile(roleField)) return roleField\n try {\n const raw = await readFile(roleField, \"utf8\")\n // Strip optional YAML frontmatter — lets a Claude Code agent\n // definition file (.claude/agents/*.md) double as a swarm role\n // without ferrying the agent's metadata into the prompt.\n const parsed = matter(raw)\n return parsed.content.trim()\n } catch (err) {\n // The string had a role-file extension but the file isn't readable\n // — surface a hint on stderr so authors don't silently get the\n // literal path as a prompt. Still fall back to inline so the swarm\n // doesn't crash on a typo.\n const code = (err as NodeJS.ErrnoException).code\n process.stderr.write(\n `agent-cli participant: role path '${roleField}' not readable (${code ?? \"unknown\"}); using the literal string as inline role text.\\n`\n )\n return roleField\n }\n}\n\nfunction looksLikeRoleFile(s: string): boolean {\n const lower = s.toLowerCase()\n return ROLE_FILE_EXTENSIONS.some((ext) => lower.endsWith(ext))\n}\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { StateStore, ParticipantId } from '../ports.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Filesystem state store — one JSON file per participant.
|
|
5
|
+
*
|
|
6
|
+
* <stateDir>/<participantId>.json
|
|
7
|
+
*
|
|
8
|
+
* Missing files read as {}. Writes are full overwrites; callers
|
|
9
|
+
* pass the merged state, the adapter doesn't merge on its own.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
type FileStateStoreOptions = {
|
|
13
|
+
/** Directory to hold the per-participant JSON files. */
|
|
14
|
+
readonly dir: string;
|
|
15
|
+
};
|
|
16
|
+
declare class FileStateStore implements StateStore {
|
|
17
|
+
private readonly opts;
|
|
18
|
+
readonly kind = "fs";
|
|
19
|
+
constructor(opts: FileStateStoreOptions);
|
|
20
|
+
read(participantId: ParticipantId): Promise<Readonly<Record<string, unknown>>>;
|
|
21
|
+
write(participantId: ParticipantId, state: Readonly<Record<string, unknown>>): Promise<void>;
|
|
22
|
+
private pathFor;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { FileStateStore, type FileStateStoreOptions };
|