@nebutra/agent-runtime 0.2.0 → 0.2.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/LICENSE +21 -676
- package/README.md +2 -0
- package/dist/adapters/dispatcher-sse.js +1 -0
- package/dist/adapters/index.d.ts +24 -5
- package/dist/adapters/index.js +28 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/mcp-catalog.js +1 -0
- package/dist/adapters/prisma-rollout.js +1 -0
- package/dist/chunk-424PT5DM.js +23 -0
- package/dist/chunk-424PT5DM.js.map +1 -0
- package/dist/{chunk-NN7DATXA.js → chunk-4Y25ZTKI.js} +3 -3
- package/dist/chunk-4Y25ZTKI.js.map +1 -0
- package/dist/{chunk-BJBBR3QA.js → chunk-D4YAPLOW.js} +4 -4
- package/dist/chunk-D4YAPLOW.js.map +1 -0
- package/dist/{chunk-ZMYX5VBU.js → chunk-GQZKYWFT.js} +23 -7
- package/dist/chunk-GQZKYWFT.js.map +1 -0
- package/dist/chunk-KCNN4QUQ.js +255 -0
- package/dist/chunk-KCNN4QUQ.js.map +1 -0
- package/dist/{chunk-PGGWSUTM.js → chunk-NI4EDT4T.js} +2 -2
- package/dist/chunk-NI4EDT4T.js.map +1 -0
- package/dist/chunk-Q62VKHIT.js +178 -0
- package/dist/chunk-Q62VKHIT.js.map +1 -0
- package/dist/{chunk-MUF7ZZTO.js → chunk-R5HOSQUW.js} +2 -1
- package/dist/{chunk-MUF7ZZTO.js.map → chunk-R5HOSQUW.js.map} +1 -1
- package/dist/{chunk-5N4644PB.js → chunk-SD2ZJ7XG.js} +4 -4
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +52 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.js +1 -0
- package/dist/definitions.js +1 -0
- package/dist/dispatcher.js +1 -0
- package/dist/durable-turn.js +3 -2
- package/dist/hook-pipeline.js +1 -0
- package/dist/index.d.ts +8 -85
- package/dist/index.js +249 -134
- package/dist/index.js.map +1 -1
- package/dist/loop.d.ts +2 -2
- package/dist/loop.js +3 -2
- package/dist/mcp-bridge.d.ts +3 -3
- package/dist/mcp-bridge.js +3 -2
- package/dist/model.js +1 -0
- package/dist/orchestration.d.ts +84 -0
- package/dist/orchestration.js +16 -0
- package/dist/orchestration.js.map +1 -0
- package/dist/policy.js +1 -0
- package/dist/protocol.js +1 -0
- package/dist/pulsar.d.ts +78 -0
- package/dist/pulsar.js +17 -0
- package/dist/pulsar.js.map +1 -0
- package/dist/rollout-store-persistent.js +1 -0
- package/dist/rollout.js +1 -0
- package/dist/sandbox.js +2 -1
- package/dist/skills.js +2 -1
- package/dist/subagents.js +1 -0
- package/dist/tools.d.ts +4 -3
- package/dist/tools.js +5 -3
- package/package.json +84 -27
- package/.turbo/turbo-build.log +0 -115
- package/.turbo/turbo-test.log +0 -44
- package/.turbo/turbo-typecheck.log +0 -4
- package/CHANGELOG.md +0 -253
- package/dist/chunk-BJBBR3QA.js.map +0 -1
- package/dist/chunk-NN7DATXA.js.map +0 -1
- package/dist/chunk-PGGWSUTM.js.map +0 -1
- package/dist/chunk-ZMYX5VBU.js.map +0 -1
- package/src/adapters/dispatcher-sse.test.ts +0 -218
- package/src/adapters/dispatcher-sse.ts +0 -222
- package/src/adapters/index.ts +0 -18
- package/src/adapters/mcp-catalog.test.ts +0 -213
- package/src/adapters/mcp-catalog.ts +0 -188
- package/src/adapters/prisma-rollout.test.ts +0 -153
- package/src/adapters/prisma-rollout.ts +0 -104
- package/src/agent-runtime.test.ts +0 -176
- package/src/artifact-stream.test.ts +0 -330
- package/src/artifact-stream.ts +0 -453
- package/src/channel-gateway.test.ts +0 -432
- package/src/channel-gateway.ts +0 -357
- package/src/code-review.test.ts +0 -501
- package/src/code-review.ts +0 -495
- package/src/command-suggestions.test.ts +0 -251
- package/src/command-suggestions.ts +0 -338
- package/src/commands.test.ts +0 -184
- package/src/commands.ts +0 -140
- package/src/commit-message.test.ts +0 -249
- package/src/commit-message.ts +0 -180
- package/src/context-compaction.test.ts +0 -522
- package/src/context-compaction.ts +0 -434
- package/src/definitions.test.ts +0 -78
- package/src/definitions.ts +0 -190
- package/src/deployment-status.test.ts +0 -215
- package/src/deployment-status.ts +0 -227
- package/src/design-context.test.ts +0 -195
- package/src/design-context.ts +0 -198
- package/src/dispatcher.test.ts +0 -234
- package/src/dispatcher.ts +0 -189
- package/src/durable-turn.test.ts +0 -209
- package/src/durable-turn.ts +0 -135
- package/src/edit-planner.test.ts +0 -204
- package/src/edit-planner.ts +0 -325
- package/src/fuzzy-match.test.ts +0 -311
- package/src/fuzzy-match.ts +0 -444
- package/src/hook-pipeline.test.ts +0 -279
- package/src/hook-pipeline.ts +0 -373
- package/src/inbound-admission.test.ts +0 -394
- package/src/inbound-admission.ts +0 -246
- package/src/index.ts +0 -47
- package/src/loop.test.ts +0 -161
- package/src/loop.ts +0 -211
- package/src/mcp-bridge.test.ts +0 -165
- package/src/mcp-bridge.ts +0 -76
- package/src/memory-provider.test.ts +0 -232
- package/src/memory-provider.ts +0 -257
- package/src/model.ts +0 -168
- package/src/permission-ruleset.test.ts +0 -301
- package/src/permission-ruleset.ts +0 -200
- package/src/policy.ts +0 -151
- package/src/project-repo.test.ts +0 -232
- package/src/project-repo.ts +0 -311
- package/src/protocol.ts +0 -159
- package/src/rollout-store-persistent.test.ts +0 -217
- package/src/rollout-store-persistent.ts +0 -166
- package/src/rollout.ts +0 -150
- package/src/sandbox.ts +0 -113
- package/src/session-share.test.ts +0 -360
- package/src/session-share.ts +0 -310
- package/src/skill-distillation.test.ts +0 -177
- package/src/skill-distillation.ts +0 -369
- package/src/skills.test.ts +0 -277
- package/src/skills.ts +0 -255
- package/src/subagents.test.ts +0 -290
- package/src/subagents.ts +0 -332
- package/src/tools.ts +0 -126
- package/src/workbench.test.ts +0 -0
- package/src/workbench.ts +0 -0
- package/tsconfig.json +0 -12
- package/tsup.config.ts +0 -33
- /package/dist/{chunk-5N4644PB.js.map → chunk-SD2ZJ7XG.js.map} +0 -0
package/.turbo/turbo-test.log
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @nebutra/agent-runtime@0.2.0 test /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/ai/agent-runtime
|
|
3
|
-
> vitest run
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
[1m[30m[46m RUN [49m[39m[22m [36mv4.1.4 [39m[90m/home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/ai/agent-runtime[39m
|
|
7
|
-
|
|
8
|
-
[32m✓[39m src/code-review.test.ts [2m([22m[2m43 tests[22m[2m)[22m[32m 183[2mms[22m[39m
|
|
9
|
-
[32m✓[39m src/context-compaction.test.ts [2m([22m[2m38 tests[22m[2m)[22m[32m 154[2mms[22m[39m
|
|
10
|
-
[32m✓[39m src/channel-gateway.test.ts [2m([22m[2m32 tests[22m[2m)[22m[32m 172[2mms[22m[39m
|
|
11
|
-
[32m✓[39m src/permission-ruleset.test.ts [2m([22m[2m41 tests[22m[2m)[22m[32m 70[2mms[22m[39m
|
|
12
|
-
[32m✓[39m src/fuzzy-match.test.ts [2m([22m[2m36 tests[22m[2m)[22m[32m 77[2mms[22m[39m
|
|
13
|
-
[32m✓[39m src/inbound-admission.test.ts [2m([22m[2m45 tests[22m[2m)[22m[32m 61[2mms[22m[39m
|
|
14
|
-
[32m✓[39m src/session-share.test.ts [2m([22m[2m29 tests[22m[2m)[22m[32m 101[2mms[22m[39m
|
|
15
|
-
[32m✓[39m src/artifact-stream.test.ts [2m([22m[2m27 tests[22m[2m)[22m[32m 120[2mms[22m[39m
|
|
16
|
-
[32m✓[39m src/skills.test.ts [2m([22m[2m16 tests[22m[2m)[22m[32m 96[2mms[22m[39m
|
|
17
|
-
[32m✓[39m src/command-suggestions.test.ts [2m([22m[2m25 tests[22m[2m)[22m[32m 69[2mms[22m[39m
|
|
18
|
-
[32m✓[39m src/commit-message.test.ts [2m([22m[2m29 tests[22m[2m)[22m[32m 98[2mms[22m[39m
|
|
19
|
-
[32m✓[39m src/subagents.test.ts [2m([22m[2m20 tests[22m[2m)[22m[32m 82[2mms[22m[39m
|
|
20
|
-
[32m✓[39m src/hook-pipeline.test.ts [2m([22m[2m18 tests[22m[2m)[22m[32m 165[2mms[22m[39m
|
|
21
|
-
[32m✓[39m src/memory-provider.test.ts [2m([22m[2m18 tests[22m[2m)[22m[32m 100[2mms[22m[39m
|
|
22
|
-
[32m✓[39m src/adapters/dispatcher-sse.test.ts [2m([22m[2m11 tests[22m[2m)[22m[32m 194[2mms[22m[39m
|
|
23
|
-
[32m✓[39m src/project-repo.test.ts [2m([22m[2m13 tests[22m[2m)[22m[32m 35[2mms[22m[39m
|
|
24
|
-
[32m✓[39m src/dispatcher.test.ts [2m([22m[2m16 tests[22m[2m)[22m[32m 190[2mms[22m[39m
|
|
25
|
-
[32m✓[39m src/commands.test.ts [2m([22m[2m16 tests[22m[2m)[22m[32m 52[2mms[22m[39m
|
|
26
|
-
[32m✓[39m src/workbench.test.ts [2m([22m[2m19 tests[22m[2m)[22m[32m 42[2mms[22m[39m
|
|
27
|
-
[32m✓[39m src/rollout-store-persistent.test.ts [2m([22m[2m12 tests[22m[2m)[22m[32m 41[2mms[22m[39m
|
|
28
|
-
[32m✓[39m src/adapters/mcp-catalog.test.ts [2m([22m[2m9 tests[22m[2m)[22m[32m 48[2mms[22m[39m
|
|
29
|
-
[32m✓[39m src/edit-planner.test.ts [2m([22m[2m21 tests[22m[2m)[22m[32m 33[2mms[22m[39m
|
|
30
|
-
[32m✓[39m src/durable-turn.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 31[2mms[22m[39m
|
|
31
|
-
[32m✓[39m src/deployment-status.test.ts [2m([22m[2m20 tests[22m[2m)[22m[32m 73[2mms[22m[39m
|
|
32
|
-
[32m✓[39m src/skill-distillation.test.ts [2m([22m[2m18 tests[22m[2m)[22m[32m 108[2mms[22m[39m
|
|
33
|
-
[32m✓[39m src/design-context.test.ts [2m([22m[2m17 tests[22m[2m)[22m[32m 47[2mms[22m[39m
|
|
34
|
-
[32m✓[39m src/agent-runtime.test.ts [2m([22m[2m15 tests[22m[2m)[22m[32m 112[2mms[22m[39m
|
|
35
|
-
[32m✓[39m src/mcp-bridge.test.ts [2m([22m[2m7 tests[22m[2m)[22m[32m 41[2mms[22m[39m
|
|
36
|
-
[32m✓[39m src/definitions.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 18[2mms[22m[39m
|
|
37
|
-
[32m✓[39m src/adapters/prisma-rollout.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 21[2mms[22m[39m
|
|
38
|
-
[32m✓[39m src/loop.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 21[2mms[22m[39m
|
|
39
|
-
|
|
40
|
-
[2m Test Files [22m [1m[32m31 passed[39m[22m[90m (31)[39m
|
|
41
|
-
[2m Tests [22m [1m[32m632 passed[39m[22m[90m (632)[39m
|
|
42
|
-
[2m Start at [22m 16:51:20
|
|
43
|
-
[2m Duration [22m 9.97s[2m (transform 4.25s, setup 0ms, import 10.84s, tests 2.65s, environment 12ms)[22m
|
|
44
|
-
|
package/CHANGELOG.md
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
# @nebutra/agent-runtime
|
|
2
|
-
|
|
3
|
-
## 0.2.0
|
|
4
|
-
|
|
5
|
-
### Minor Changes
|
|
6
|
-
|
|
7
|
-
- [`fab751f`](https://github.com/Nebutra/Nebutra-Sailor/commit/fab751f39639251bfa006b1bfbcd8cd62f94626a) Thanks [@TsekaLuk](https://github.com/TsekaLuk)! - New adapter subpaths inside `@nebutra/agent-runtime`: concrete reusable port
|
|
8
|
-
adapters for the runtime package.
|
|
9
|
-
|
|
10
|
-
- `mcp-catalog`: adapts `@nebutra/mcp` `serverRegistry`/`mcpClient` into the
|
|
11
|
-
`McpServerCatalogPort` / `McpClientLike` ports — plan-gated, tenant
|
|
12
|
-
fail-closed, schema-less MCP tools still yield usable definitions,
|
|
13
|
-
composes with `activateMcpTools`.
|
|
14
|
-
- `dispatcher-sse`: runtime-agnostic transport for `ProtocolDispatcher` using
|
|
15
|
-
only Web-standard `Request`/`Response` — JSON-RPC handler (never throws),
|
|
16
|
-
incremental `text/event-stream` SSE with error-frame + `AbortSignal`
|
|
17
|
-
cancellation, listener→async-iterable notification bridge.
|
|
18
|
-
|
|
19
|
-
Dependency-injected (no hidden globals), tenant fail-closed, zero framework
|
|
20
|
-
lock-in. The durable rollout-store backend adapter is intentionally not
|
|
21
|
-
included (a correct system-of-record needs a fail-loud datastore; pending a
|
|
22
|
-
governance decision — documented in the README).
|
|
23
|
-
|
|
24
|
-
- [`4865ac9`](https://github.com/Nebutra/Nebutra-Sailor/commit/4865ac9bff6dd83635bc33e4aeb348b087dee7c7) Thanks [@TsekaLuk](https://github.com/TsekaLuk)! - Absorb the IDE-coding-agent delta (over the already-absorbed CLI core).
|
|
25
|
-
|
|
26
|
-
- `code-review`: a pure unified-diff parser, branch-base resolution, a
|
|
27
|
-
confidence-banded review prompt (CRITICAL ≥95% / WARNING ≥85% /
|
|
28
|
-
SUGGESTION ≥75% / below → omit) with an untrusted-content injection guard
|
|
29
|
-
over diff + commit messages, an advisory/no-edit invariant, a fail-closed
|
|
30
|
-
finding parser (`ReviewParseError` rather than a fabricated empty result),
|
|
31
|
-
and a deterministic post-review mode handoff.
|
|
32
|
-
- `context-compaction`: in-session, mid-turn context-overflow recovery — a
|
|
33
|
-
1.3× token-correction estimator, a 60%-floored budget, greedy whole-message
|
|
34
|
-
packing with an oversized-message clipped-transcript fallback,
|
|
35
|
-
bounded-concurrency chunk summarization with a preserve-rubric prompt, a
|
|
36
|
-
recursive binary reduce capped at an output token limit, and a fail-closed
|
|
37
|
-
`"compact"` retry sentinel (never silently drops context).
|
|
38
|
-
- `commit-message` (WRAP): a Conventional-Commits prompt + git-context shape +
|
|
39
|
-
"regenerate materially different" negative constraint + fence/quote
|
|
40
|
-
stripping + bounded retry over the existing injected small-model seam.
|
|
41
|
-
|
|
42
|
-
Tenant-scoped / fail-closed where stateful; pure where stateless; the only
|
|
43
|
-
impurity is an injected model/summarize port. 632 package tests.
|
|
44
|
-
|
|
45
|
-
- [`67f9cd0`](https://github.com/Nebutra/Nebutra-Sailor/commit/67f9cd02c0ee908f0579efadc4292d535a758c16) Thanks [@TsekaLuk](https://github.com/TsekaLuk)! - Durable rollout-store backend (the agent-runtime system-of-record).
|
|
46
|
-
|
|
47
|
-
- New Prisma model `AgentRolloutLine` → table `agent_rollout_lines`, additive
|
|
48
|
-
migration `20260519000000_add_agent_rollout_store` (one CREATE TABLE).
|
|
49
|
-
- `createPrismaRolloutPersistence` in `@nebutra/agent-runtime-adapters`:
|
|
50
|
-
fail-loud `RolloutPersistencePort` over a minimal injected
|
|
51
|
-
`PrismaRolloutDelegate` (no generated-client / `@nebutra/db` dependency;
|
|
52
|
-
per-tenant resolver for RLS). 6 TDD tests.
|
|
53
|
-
- Gateway route store is env-gated: `AGENT_ROLLOUT_DURABLE=1` selects the
|
|
54
|
-
durable Postgres store, default stays in-memory. Activation is a standard
|
|
55
|
-
migrate+generate deploy step (ADR 2026-05-19), not faked durability.
|
|
56
|
-
|
|
57
|
-
- [`bb1b126`](https://github.com/Nebutra/Nebutra-Sailor/commit/bb1b126e82379d88da10f43a86d9d07bccc3a224) Thanks [@TsekaLuk](https://github.com/TsekaLuk)! - Absorb the git-backed-project delta (over the already-absorbed core).
|
|
58
|
-
|
|
59
|
-
- `project-repo`: durable project = git repo. Metadata + per-conversation
|
|
60
|
-
jsonl logs persisted as committed files (one commit per turn); history /
|
|
61
|
-
restore by commit SHA (commit graph IS the history — distinct from
|
|
62
|
-
workbench's in-memory snapshot ring); injected `GitHostPort` (no git
|
|
63
|
-
vendor lock-in); tenant ownership gate (`ownsRepo`, default-deny).
|
|
64
|
-
- `deployment-status`: commit-keyed lifecycle (`idle|deploying|live|
|
|
65
|
-
failed`), latest-status + timeline-from-commits derivations, explicit
|
|
66
|
-
`advanceState` machine; preview domain from an injected suffix (no
|
|
67
|
-
hardcoded host).
|
|
68
|
-
|
|
69
|
-
Tenant-scoped & fail-closed; pure data/logic; git + deploy hosts injected.
|
|
70
|
-
Resolved a `CommitRef` barrel collision (deployment-status structural type
|
|
71
|
-
renamed `DeploymentCommitRef`). 252 package tests.
|
|
72
|
-
|
|
73
|
-
- [`da6bfea`](https://github.com/Nebutra/Nebutra-Sailor/commit/da6bfeaf6c323a9aecefdd65c481a9852aee25b9) Thanks [@TsekaLuk](https://github.com/TsekaLuk)! - Add `@nebutra/agent-runtime`: a multi-tenant agent-runtime grammar.
|
|
74
|
-
|
|
75
|
-
- New package re-expresses a coding-agent runtime _design_ in Sailor grammar —
|
|
76
|
-
thread/turn/item model + event lifecycle, two-axis approval/capability
|
|
77
|
-
policy, uniform tool/MCP abstraction, event-sourced rollout with compaction,
|
|
78
|
-
and an external-sandbox delegation seam.
|
|
79
|
-
- Every serialization scope, store key, and dispatch is tenant-scoped;
|
|
80
|
-
cross-tenant requests can never share a serial lane.
|
|
81
|
-
- No infra change and no in-process untrusted-code execution: the default
|
|
82
|
-
executor fails closed; real execution is delegated behind `ExternalSandbox`.
|
|
83
|
-
- Adds an off-by-default `agent-runtime-demo` feature flag gating a demo route
|
|
84
|
-
in `apps/web`.
|
|
85
|
-
|
|
86
|
-
- [`3c5c43e`](https://github.com/Nebutra/Nebutra-Sailor/commit/3c5c43e1c07c03da936d2948408f8abb4b113321) Thanks [@TsekaLuk](https://github.com/TsekaLuk)! - Absorb the differentiated coding-agent-harness capabilities (the delta over
|
|
87
|
-
the already-absorbed core).
|
|
88
|
-
|
|
89
|
-
- `definitions`: shared layered-tier resolver (precedence + dual
|
|
90
|
-
availability/enabled gate) + frontmatter parser — backs commands, skills,
|
|
91
|
-
subagents from one kernel.
|
|
92
|
-
- `skills`: two-phase progressive disclosure (token-budget listing,
|
|
93
|
-
first-party never-truncated, path-activation), lazy body expansion,
|
|
94
|
-
per-skill model/tool override merge.
|
|
95
|
-
- `hook-pipeline`: 18-event taxonomy, config matcher/`if` resolver,
|
|
96
|
-
structured decision protocol, multi-transport (function/http/prompt),
|
|
97
|
-
parallel fan-out with deterministic precedence merge; wraps the bare
|
|
98
|
-
`ToolHooks` as the in-process transport.
|
|
99
|
-
- `commands`: unified command=skill model with dual user/model front-doors
|
|
100
|
-
and a pure arg-substitution + templating expander.
|
|
101
|
-
- `subagents`: definition resolver (tool allow−deny), fork-vs-spawn
|
|
102
|
-
context-boundary contract, typed terminal-envelope result contract, and a
|
|
103
|
-
tenant-scoped Task lifecycle registry.
|
|
104
|
-
|
|
105
|
-
All tenant-scoped & fail-closed; pure data/logic (no FS/shell/TUI — those
|
|
106
|
-
transports deliberately dropped for multi-tenant safety). 135 package tests.
|
|
107
|
-
|
|
108
|
-
- [`a29b2d9`](https://github.com/Nebutra/Nebutra-Sailor/commit/a29b2d997416edc4e421b31b777724ae70e4da54) Thanks [@TsekaLuk](https://github.com/TsekaLuk)! - Add the agent loop runner — the turn engine.
|
|
109
|
-
|
|
110
|
-
- `runTurn()` drives a turn `loop { model_call → emit items → execute tools
|
|
111
|
-
→ feed results back }` until the model stops requesting tools or a bounded
|
|
112
|
-
step ceiling is hit. Single-threaded (shared context, no conflicting
|
|
113
|
-
sub-agent decisions).
|
|
114
|
-
- The model call is abstracted behind `ModelInvoker`, WRAPping an existing
|
|
115
|
-
model stack rather than re-porting provider/routing/fallback.
|
|
116
|
-
- Every terminal item and the turn outcome are appended to the tenant-scoped
|
|
117
|
-
rollout (resumable by replay); tool calls pass the approval gate
|
|
118
|
-
(`resolveRuleDecision` + server-initiated `ApprovalGate`); unapproved tools
|
|
119
|
-
are never dispatched. Internal failures surface as `turn.failed`, never
|
|
120
|
-
thrown to the caller.
|
|
121
|
-
|
|
122
|
-
- [`cd34724`](https://github.com/Nebutra/Nebutra-Sailor/commit/cd347242ab6874931a74aa72aa9a0d183f7dd5d3) Thanks [@TsekaLuk](https://github.com/TsekaLuk)! - Absorb the multi-channel-gateway delta (over the already-absorbed core).
|
|
123
|
-
|
|
124
|
-
- `channel-gateway`: channel-agnostic `InboundMessage`/`OutboundMessage`
|
|
125
|
-
normalization, a `ChannelAdapter` port (parseInbound returns null for
|
|
126
|
-
non-message events, never throws on junk), tenant-scoped `ChannelRegistry`,
|
|
127
|
-
and outbound route-back through the originating channel.
|
|
128
|
-
- `inbound-admission`: deterministic tenant-prefixed `resolveSessionKey`
|
|
129
|
-
(group/DM/thread bindings, cross-tenant non-colliding), allowlist gating
|
|
130
|
-
(exact / `*` / `prefix:*`, closed-by-default), mention gating (group
|
|
131
|
-
requires @mention or reply-to-assistant; DM never), a time-injected
|
|
132
|
-
`InboundDebouncer`, composed by `admitInbound`.
|
|
133
|
-
|
|
134
|
-
Tenant-scoped & fail-closed; pure data/logic; transport + time injected.
|
|
135
|
-
Only the clean contracts were re-expressed (the messy source impl is not
|
|
136
|
-
ported). 365 package tests.
|
|
137
|
-
|
|
138
|
-
- [`447e948`](https://github.com/Nebutra/Nebutra-Sailor/commit/447e948a7153446e7bc3769395c1687c9f27c991) Thanks [@TsekaLuk](https://github.com/TsekaLuk)! - Close the runtime gap — four capabilities, all tenant-scoped & fail-closed,
|
|
139
|
-
each behind an injectable port so the package keeps zero datastore/queue
|
|
140
|
-
dependency ("no infra change" stays honest):
|
|
141
|
-
|
|
142
|
-
- **Durable / resumable turn** (`durable-turn.ts`): `createDurableTurn` wraps
|
|
143
|
-
`runTurn` behind a `DurableTurnQueuePort`; `resume` replays the rollout and
|
|
144
|
-
re-drives an unfinished turn, idempotent and cross-tenant isolated.
|
|
145
|
-
- **Protocol dispatcher** (`dispatcher.ts`): transport-agnostic JSON-RPC
|
|
146
|
-
dispatcher serving the method registry — envelope validation fail-closed,
|
|
147
|
-
per-`scopeKey` serialization (cross-tenant never shares a lane), errors
|
|
148
|
-
mapped to JSON-RPC codes, validated notification stream.
|
|
149
|
-
- **MCP activation** (`mcp-bridge.ts`): `activateMcpTools` adapts tenant/plan
|
|
150
|
-
-visible MCP-server tools into the uniform `ToolRegistry` via an injectable
|
|
151
|
-
`McpServerCatalogPort`; duplicate names skipped, empty tenant fails closed.
|
|
152
|
-
- **Persistent rollout store** (`rollout-store-persistent.ts`):
|
|
153
|
-
`PersistentRolloutStore` over a `RolloutPersistencePort` (satisfiable by an
|
|
154
|
-
`@nebutra/audit`/`@nebutra/db` adapter) — monotonic per-(tenant,thread)
|
|
155
|
-
seq, faithful round-trip, cross-tenant isolation, typed round-trip errors.
|
|
156
|
-
|
|
157
|
-
- [`b1b9b72`](https://github.com/Nebutra/Nebutra-Sailor/commit/b1b9b7251ef6b1b84e5211dd9b4ae14a1be4b05a) Thanks [@TsekaLuk](https://github.com/TsekaLuk)! - Absorb the self-improving-loop delta (over the already-absorbed core).
|
|
158
|
-
|
|
159
|
-
- `memory-provider`: pluggable cross-session `MemoryProvider` port (full
|
|
160
|
-
lifecycle: initialize/prefetch/syncTurn/onSessionEnd/onSessionSwitch/
|
|
161
|
-
onPreCompress/onDelegation/systemPromptBlock) + a `MemoryManager` that
|
|
162
|
-
degrades safely + memory-context injection defense (recalled text
|
|
163
|
-
banner-wrapped as reference-not-instructions; a split-safe streaming
|
|
164
|
-
scrubber strips model-forged memory banners). Vendor backends injected.
|
|
165
|
-
- `skill-distillation`: the learning loop — eligibility gate (only
|
|
166
|
-
non-trivial successful trajectories), `distillSkill` (deterministic
|
|
167
|
-
prompt → injected synthesize → skill record with `allowedTools` clamped
|
|
168
|
-
to tools the experience actually used = least privilege), `improveSkill`
|
|
169
|
-
(versioned, tenant-locked), pure `shouldNudgePersist` heuristic.
|
|
170
|
-
|
|
171
|
-
Tenant-scoped & fail-closed; pure data/logic; LLM synthesis + memory
|
|
172
|
-
backends are injected ports. 288 package tests.
|
|
173
|
-
|
|
174
|
-
- [`5ed8c38`](https://github.com/Nebutra/Nebutra-Sailor/commit/5ed8c38362e86bad51786383b81cc921b11d1b18) Thanks [@TsekaLuk](https://github.com/TsekaLuk)! - Absorb the coding-agent share/permission delta (over the already-absorbed core).
|
|
175
|
-
|
|
176
|
-
- `permission-ruleset`: `Rule{permission,pattern,action:allow|deny|ask}` +
|
|
177
|
-
`evaluate(...rulesets)` first-match over both dimensions with fail-safe
|
|
178
|
-
`ask` default, a `wildcardMatch` with the faithful trailing `" *"`-optional
|
|
179
|
-
rule (no catastrophic backtracking), and a bash-command-prefix/arity
|
|
180
|
-
extractor (`commandPrefix`/`commandPermissionKey`).
|
|
181
|
-
- `session-share`: mint a read-only `ShareRecord{id,url,secret}` for a
|
|
182
|
-
session, revoke it, public secret-gated `verifyViewer` (no tenantId —
|
|
183
|
-
constant-time compare; documented threat model). Injected id-mint /
|
|
184
|
-
url-builder / sync-sink / store ports; kill-switch; persist-first
|
|
185
|
-
best-effort sync.
|
|
186
|
-
|
|
187
|
-
Owner plane tenant-scoped & fail-closed; viewer plane secret-gated; pure
|
|
188
|
-
where stateless; transport/crypto/store injected. 496 package tests.
|
|
189
|
-
|
|
190
|
-
- [`77c0105`](https://github.com/Nebutra/Nebutra-Sailor/commit/77c0105ce89a4ff0f5e86229cce9c2561c1b1c5f) Thanks [@TsekaLuk](https://github.com/TsekaLuk)! - Absorb the site-clone / targeted-edit delta (over the already-absorbed core).
|
|
191
|
-
|
|
192
|
-
- `design-context`: website → structured generation seed
|
|
193
|
-
`{content, brand{colors,fonts}, screenshot, title}` — pure normalizer +
|
|
194
|
-
injected `ScrapeProvider` port (no network/provider lock-in),
|
|
195
|
-
`toGenerationSeed` bounded deterministic prompt block.
|
|
196
|
-
- `edit-planner`: pattern-driven `analyzeEditIntent` (7 EditTypes) +
|
|
197
|
-
`selectFilesForEdit` (primary/context split + deterministic system-prompt
|
|
198
|
-
builder) + generalised fast-apply (`parseEditBlocks` / `applyEditBlock`
|
|
199
|
-
with an injected merger). Targeted edit planning vs regenerate-all.
|
|
200
|
-
|
|
201
|
-
Tenant-scoped & fail-closed; pure data/logic; scrape + LLM-merge are
|
|
202
|
-
injected ports. 219 package tests.
|
|
203
|
-
|
|
204
|
-
- [`1e2d929`](https://github.com/Nebutra/Nebutra-Sailor/commit/1e2d9298f4f093cbe81199e7dcd3991116d01ea2) Thanks [@TsekaLuk](https://github.com/TsekaLuk)! - Absorb the terminal-suggestion delta (over the already-absorbed core).
|
|
205
|
-
|
|
206
|
-
- `fuzzy-match`: smart-case subsequence fuzzy match returning
|
|
207
|
-
`{score, indices}` (lowercase query ⇒ case-insensitive; any uppercase ⇒
|
|
208
|
-
case-sensitive), case-insensitive + ignore-spaces variants (indices map to
|
|
209
|
-
original text), anchored glob-wildcard (`* ? [a-z] \`, no catastrophic
|
|
210
|
-
backtracking), stable `rankByFuzzy`. Pure, dependency-free.
|
|
211
|
-
- `command-suggestions`: `classifyMatch` with deterministic
|
|
212
|
-
exact > prefix > fuzzy score banding, `rankSuggestions` (history-boost +
|
|
213
|
-
shorter-text + stable tie-breaks), `dedupeByText` (history-wins,
|
|
214
|
-
case-insensitive), tenant-scoped `SuggestionHistoryStore` (recency ring,
|
|
215
|
-
cross-tenant isolation). Fuzzy matcher injected.
|
|
216
|
-
|
|
217
|
-
Tenant-scoped & fail-closed where stateful; pure data/logic. The source's
|
|
218
|
-
generated `command-signatures-v2` JS bundle was deliberately not ported.
|
|
219
|
-
426 package tests.
|
|
220
|
-
|
|
221
|
-
- [`509338e`](https://github.com/Nebutra/Nebutra-Sailor/commit/509338ee5ffc537c108f06cd1fc24a811a5e05b8) Thanks [@TsekaLuk](https://github.com/TsekaLuk)! - Concrete Track-B coupling for the agent-runtime external-sandbox seam.
|
|
222
|
-
|
|
223
|
-
- Add `createHttpSandbox(baseUrl)`: an `ExternalSandbox` that delegates
|
|
224
|
-
execution over HTTP to the decoupled Rust isolator
|
|
225
|
-
(`backends/rust/sandbox`, `POST /api/v1/sandbox/exec`).
|
|
226
|
-
- Non-2xx isolator responses (e.g. a fail-closed 403 refusal) surface as
|
|
227
|
-
`SandboxDelegationError` and are never coerced into a fabricated result.
|
|
228
|
-
- The Rust isolator now mirrors the `SandboxExecRequest` /
|
|
229
|
-
`SandboxExecResult` / `CapabilityPolicy` wire shapes and is fail-closed
|
|
230
|
-
until a real isolation backend (Wasmtime/Firecracker) is wired.
|
|
231
|
-
|
|
232
|
-
- [`efe764d`](https://github.com/Nebutra/Nebutra-Sailor/commit/efe764d60085352eae304dc37dc22fda8920e587) Thanks [@TsekaLuk](https://github.com/TsekaLuk)! - Absorb the web-app-builder delta (the differentiated capabilities over the
|
|
233
|
-
already-absorbed harness core).
|
|
234
|
-
|
|
235
|
-
- `artifact-stream`: streaming artifact/action protocol — a chunk-fed
|
|
236
|
-
incremental parser for an `<artifact>` of ordered
|
|
237
|
-
`<action type=file|shell|start|build|data>` blocks (split-tag safe, no
|
|
238
|
-
duplicate emit), plus an action runner with a
|
|
239
|
-
`pending→running→complete|aborted|failed` state-machine, submission-order
|
|
240
|
-
queue, halt-on-failure, and execution delegated through injected ports
|
|
241
|
-
(no in-process exec / host FS). Distinct from tool-calling.
|
|
242
|
-
- `workbench`: tenant-scoped project-state model — immutable FileMap with
|
|
243
|
-
auto-derived folders, apply-file-mutation, snapshot/restore with
|
|
244
|
-
deep-copy isolation, bounded snapshot history, project diff; behind a
|
|
245
|
-
`ProjectPersistencePort` (in-memory ref impl mirroring the rollout-store
|
|
246
|
-
seam).
|
|
247
|
-
|
|
248
|
-
All tenant-scoped & fail-closed, pure data/logic. 181 package tests.
|
|
249
|
-
|
|
250
|
-
### Patch Changes
|
|
251
|
-
|
|
252
|
-
- Updated dependencies [[`5d3d7e6`](https://github.com/Nebutra/Nebutra-Sailor/commit/5d3d7e6c59cae5aa242bb988b75a9888cfd0db39)]:
|
|
253
|
-
- @nebutra/mcp@0.1.1
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/loop.ts"],"sourcesContent":["/**\n * Agent loop runner (WRAP — the turn engine).\n *\n * Faithful re-expression of the upstream loop: a turn is\n * `loop { model_call → emit items → execute tools → feed results back }`\n * until the model stops requesting tools or a bounded step ceiling is hit.\n * Single-threaded (Cognition teaching: shared context, no conflicting\n * sub-agent decisions). Every item is appended to the tenant-scoped rollout\n * as it reaches a terminal state, so the turn is resumable by replay.\n *\n * The model call is abstracted behind {@link ModelInvoker} so this WRAPs an\n * existing model stack (e.g. `@nebutra/agents`) rather than re-porting\n * provider/routing/fallback. No untrusted code runs here — command items are\n * dispatched through the tool registry / external-sandbox seam.\n */\n\nimport type { AgentMessageItem, ThreadEvent, ThreadItem, TurnConfig, TurnUsage } from \"./model\";\nimport {\n type ApprovalPolicy,\n DENIED,\n isApproval,\n type ReviewDecision,\n type RuleDecision,\n resolveRuleDecision,\n} from \"./policy\";\nimport type { ServerRequest } from \"./protocol\";\nimport { type RolloutLine, type RolloutStore, sanitizeForPersist } from \"./rollout\";\nimport type { ToolDispatchContext, ToolRegistry } from \"./tools\";\n\n/** A single thing the model emitted in one round. */\nexport type ModelEmission =\n | { readonly kind: \"text\"; readonly text: string }\n | {\n readonly kind: \"tool_call\";\n readonly id: string;\n readonly name: string;\n readonly args: unknown;\n };\n\nexport interface ModelRoundResult {\n readonly emissions: readonly ModelEmission[];\n readonly usage?: Partial<TurnUsage>;\n}\n\nexport interface ModelRoundRequest {\n readonly config: TurnConfig;\n /** Running transcript: user input, agent text, and tool results. */\n readonly history: readonly { readonly role: string; readonly content: string }[];\n readonly toolNames: readonly string[];\n}\n\n/** Abstracts the model stack — implement over `@nebutra/agents`, etc. */\nexport interface ModelInvoker {\n invoke(request: ModelRoundRequest): Promise<ModelRoundResult>;\n}\n\n/** Server-initiated approval transport (see {@link ServerRequest}). */\nexport interface ApprovalGate {\n request(serverRequest: ServerRequest): Promise<ReviewDecision>;\n}\n\n/** Classifies a tool call into a static rule decision before approval. */\nexport type RuleEvaluator = (toolName: string, args: unknown) => RuleDecision;\n\nexport interface RunTurnDeps {\n readonly tenantId: string;\n readonly threadId: string;\n readonly config: TurnConfig;\n readonly approvalPolicy: ApprovalPolicy;\n readonly model: ModelInvoker;\n readonly tools: ToolRegistry;\n readonly store: RolloutStore;\n readonly approvalGate: ApprovalGate;\n /** Defaults to: everything requires a prompt (safe). */\n readonly ruleEvaluator?: RuleEvaluator;\n /** Bounded steps (parity with upstream step ceiling). Default 20. */\n readonly maxSteps?: number;\n}\n\nconst DEFAULT_MAX_STEPS = 20;\nconst requirePrompt: RuleEvaluator = () => \"prompt\";\n\nfunction newId(prefix: string): string {\n return `${prefix}_${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`}`;\n}\n\n/**\n * Drive one turn to completion. Yields the {@link ThreadEvent} stream and\n * appends each terminal item + the turn outcome to the rollout store.\n * Never throws to the caller — failures surface as a `turn.failed` event.\n */\nexport async function* runTurn(userInput: string, deps: RunTurnDeps): AsyncGenerator<ThreadEvent> {\n const at = () => new Date().toISOString();\n const ruleEvaluator = deps.ruleEvaluator ?? requirePrompt;\n const maxSteps = deps.maxSteps ?? DEFAULT_MAX_STEPS;\n const ctx: ToolDispatchContext = { tenantId: deps.tenantId, threadId: deps.threadId };\n\n const append = async (line: RolloutLine): Promise<void> => deps.store.append(line);\n const event = async (e: ThreadEvent): Promise<ThreadEvent> => {\n await append({\n tenantId: deps.tenantId,\n threadId: deps.threadId,\n type: \"event\",\n event:\n e.type === \"item.completed\"\n ? { type: \"item.completed\", item: sanitizeForPersist(e.item) }\n : e,\n at: at(),\n });\n return e;\n };\n\n yield await event({ type: \"turn.started\" });\n\n const history: { role: string; content: string }[] = [{ role: \"user\", content: userInput }];\n let usage: TurnUsage = {\n inputTokens: 0,\n cachedInputTokens: 0,\n outputTokens: 0,\n reasoningOutputTokens: 0,\n };\n\n try {\n for (let step = 0; step < maxSteps; step++) {\n const round = await deps.model.invoke({\n config: deps.config,\n history,\n toolNames: deps.tools.list().map((t) => t.definition.name),\n });\n if (round.usage) {\n const u = round.usage;\n usage = {\n inputTokens: usage.inputTokens + (u.inputTokens ?? 0),\n cachedInputTokens: usage.cachedInputTokens + (u.cachedInputTokens ?? 0),\n outputTokens: usage.outputTokens + (u.outputTokens ?? 0),\n reasoningOutputTokens: usage.reasoningOutputTokens + (u.reasoningOutputTokens ?? 0),\n };\n }\n\n const toolCalls = round.emissions.filter(\n (e): e is Extract<ModelEmission, { kind: \"tool_call\" }> => e.kind === \"tool_call\",\n );\n\n for (const e of round.emissions) {\n if (e.kind !== \"text\") continue;\n const item: AgentMessageItem = { id: newId(\"msg\"), type: \"agent_message\", text: e.text };\n history.push({ role: \"assistant\", content: e.text });\n yield await event({ type: \"item.completed\", item });\n }\n\n if (toolCalls.length === 0) break; // model is done\n\n for (const call of toolCalls) {\n const decision = await gateToolCall(call.name, call.args, deps, ruleEvaluator);\n if (!isApproval(decision)) {\n const failed: ThreadItem = {\n id: call.id,\n type: \"error\",\n message: `tool '${call.name}' not approved (${decision.kind})`,\n };\n history.push({ role: \"tool\", content: `DENIED: ${call.name}` });\n yield await event({ type: \"item.completed\", item: failed });\n continue;\n }\n try {\n const output = await deps.tools.dispatch(call.name, call.args, ctx);\n const item: ThreadItem = {\n id: call.id,\n type: \"mcp_tool_call\",\n server: \"native\",\n tool: call.name,\n arguments: call.args,\n result: { content: output },\n status: \"completed\",\n };\n history.push({ role: \"tool\", content: JSON.stringify(output) });\n yield await event({ type: \"item.completed\", item });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n history.push({ role: \"tool\", content: `ERROR: ${message}` });\n yield await event({\n type: \"item.completed\",\n item: { id: call.id, type: \"error\", message },\n });\n }\n }\n }\n\n yield await event({ type: \"turn.completed\", usage });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n yield await event({ type: \"turn.failed\", error: { message } });\n }\n}\n\n/** Resolve a tool call's approval, raising a server-initiated request if asked. */\nasync function gateToolCall(\n name: string,\n args: unknown,\n deps: RunTurnDeps,\n ruleEvaluator: RuleEvaluator,\n): Promise<ReviewDecision> {\n const outcome = resolveRuleDecision(ruleEvaluator(name, args), deps.approvalPolicy);\n if (outcome === \"auto_allow\") return { kind: \"approved\" };\n if (outcome === \"auto_reject\") return DENIED;\n return deps.approvalGate.request({\n type: \"permissions.request_approval\",\n requestId: newId(\"appr\"),\n summary: `tool '${name}'`,\n });\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tools.ts"],"sourcesContent":["/**\n * Tool / MCP abstraction (WRAP — capability #9).\n *\n * Faithful re-expression of the upstream uniform tool model: one\n * `ToolDefinition` interface, JSON-schema in/out, a deferred-load flag, and\n * MCP-as-adapter behind the same interface (MCP tools and native tools are\n * indistinguishable at dispatch). Activates `@nebutra/mcp` primitives rather\n * than re-porting them.\n */\n\nimport type { z } from \"zod\";\n\n/** A tool definition — pure data; transport-agnostic. */\nexport interface ToolDefinition<I = unknown, O = unknown> {\n readonly name: string;\n readonly description: string;\n readonly inputSchema: z.ZodType<I>;\n readonly outputSchema?: z.ZodType<O>;\n /**\n * When true, the full schema is withheld until a `tool_search` discovery\n * step lazily loads it (parity with the upstream deferred-tool pattern).\n */\n readonly deferLoading?: boolean;\n}\n\n/** Source provenance — native vs. adapted from an MCP server. */\nexport type ToolOrigin =\n | { readonly kind: \"native\" }\n | { readonly kind: \"mcp\"; readonly server: string };\n\nexport interface RegisteredTool {\n readonly definition: ToolDefinition;\n readonly origin: ToolOrigin;\n}\n\n/** A tool invocation routed through the registry. */\nexport interface ToolDispatchContext {\n /** Mandatory tenant scope — every dispatch is tenant-bound. */\n readonly tenantId: string;\n readonly threadId: string;\n}\n\nexport type ToolHandler<I = unknown, O = unknown> = (\n input: I,\n ctx: ToolDispatchContext,\n) => Promise<O>;\n\n/** Pre/post dispatch hooks (parity with upstream orchestrator hooks). */\nexport interface ToolHooks {\n readonly preToolUse?: (name: string, input: unknown, ctx: ToolDispatchContext) => Promise<void>;\n readonly postToolUse?: (name: string, output: unknown, ctx: ToolDispatchContext) => Promise<void>;\n}\n\n/**\n * registry -> router -> orchestrator pipeline. MCP tools register via\n * {@link adaptMcpTool} and are dispatched identically to native tools.\n */\nexport class ToolRegistry {\n readonly #tools = new Map<string, { reg: RegisteredTool; handler: ToolHandler }>();\n readonly #hooks: ToolHooks;\n\n constructor(hooks: ToolHooks = {}) {\n this.#hooks = hooks;\n }\n\n register<I, O>(\n definition: ToolDefinition<I, O>,\n handler: ToolHandler<I, O>,\n origin: ToolOrigin = { kind: \"native\" },\n ): void {\n if (this.#tools.has(definition.name)) {\n throw new Error(`Tool already registered: ${definition.name}`);\n }\n this.#tools.set(definition.name, {\n reg: { definition: definition as ToolDefinition, origin },\n handler: handler as ToolHandler,\n });\n }\n\n list(): readonly RegisteredTool[] {\n return [...this.#tools.values()].map((e) => e.reg);\n }\n\n async dispatch(name: string, rawInput: unknown, ctx: ToolDispatchContext): Promise<unknown> {\n const entry = this.#tools.get(name);\n if (!entry) throw new Error(`Unknown tool: ${name}`);\n const input = entry.reg.definition.inputSchema.parse(rawInput);\n await this.#hooks.preToolUse?.(name, input, ctx);\n const output = await entry.handler(input, ctx);\n const validated = entry.reg.definition.outputSchema\n ? entry.reg.definition.outputSchema.parse(output)\n : output;\n await this.#hooks.postToolUse?.(name, validated, ctx);\n return validated;\n }\n}\n\n/** Minimal shape of an MCP client (satisfied by `@nebutra/mcp`'s `mcpClient`). */\nexport interface McpClientLike {\n executeTool(\n name: string,\n args: unknown,\n ctx: { requestId: string; tenantId: string },\n ): Promise<unknown>;\n}\n\n/**\n * Adapt an external MCP tool into the uniform tool model. The resulting\n * handler delegates to the MCP client; callers cannot tell it apart from a\n * native tool.\n */\nexport function adaptMcpTool(\n server: string,\n definition: ToolDefinition,\n client: McpClientLike,\n): { definition: ToolDefinition; handler: ToolHandler; origin: ToolOrigin } {\n return {\n definition,\n origin: { kind: \"mcp\", server },\n handler: async (input, ctx) =>\n client.executeTool(definition.name, input, {\n requestId: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}`,\n tenantId: ctx.tenantId,\n }),\n };\n}\n"],"mappings":";AAyDO,IAAM,eAAN,MAAmB;AAAA,EACf,SAAS,oBAAI,IAA2D;AAAA,EACxE;AAAA,EAET,YAAY,QAAmB,CAAC,GAAG;AACjC,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,SACE,YACA,SACA,SAAqB,EAAE,MAAM,SAAS,GAChC;AACN,QAAI,KAAK,OAAO,IAAI,WAAW,IAAI,GAAG;AACpC,YAAM,IAAI,MAAM,4BAA4B,WAAW,IAAI,EAAE;AAAA,IAC/D;AACA,SAAK,OAAO,IAAI,WAAW,MAAM;AAAA,MAC/B,KAAK,EAAE,YAA0C,OAAO;AAAA,MACxD;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,OAAkC;AAChC,WAAO,CAAC,GAAG,KAAK,OAAO,OAAO,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG;AAAA,EACnD;AAAA,EAEA,MAAM,SAAS,MAAc,UAAmB,KAA4C;AAC1F,UAAM,QAAQ,KAAK,OAAO,IAAI,IAAI;AAClC,QAAI,CAAC,MAAO,OAAM,IAAI,MAAM,iBAAiB,IAAI,EAAE;AACnD,UAAM,QAAQ,MAAM,IAAI,WAAW,YAAY,MAAM,QAAQ;AAC7D,UAAM,KAAK,OAAO,aAAa,MAAM,OAAO,GAAG;AAC/C,UAAM,SAAS,MAAM,MAAM,QAAQ,OAAO,GAAG;AAC7C,UAAM,YAAY,MAAM,IAAI,WAAW,eACnC,MAAM,IAAI,WAAW,aAAa,MAAM,MAAM,IAC9C;AACJ,UAAM,KAAK,OAAO,cAAc,MAAM,WAAW,GAAG;AACpD,WAAO;AAAA,EACT;AACF;AAgBO,SAAS,aACd,QACA,YACA,QAC0E;AAC1E,SAAO;AAAA,IACL;AAAA,IACA,QAAQ,EAAE,MAAM,OAAO,OAAO;AAAA,IAC9B,SAAS,OAAO,OAAO,QACrB,OAAO,YAAY,WAAW,MAAM,OAAO;AAAA,MACzC,WAAW,WAAW,QAAQ,aAAa,KAAK,GAAG,KAAK,IAAI,CAAC;AAAA,MAC7D,UAAU,IAAI;AAAA,IAChB,CAAC;AAAA,EACL;AACF;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/mcp-bridge.ts"],"sourcesContent":["/**\n * MCP activation bridge (WRAP — capability #9, activation seam).\n *\n * Concrete bridge that registers external MCP-server tools into the runtime's\n * uniform tool model. It does NOT import `@nebutra/mcp` (WIP / do-not-import):\n * instead it defines minimal injectable ports so callers wire `@nebutra/mcp`\n * (`serverRegistry` + `mcpClient`) without this package taking a hard dep.\n *\n * Tenant-scoped by construction: the catalog port is always queried with the\n * caller's `{ tenantId, plan }`, so a tenant only ever sees its own MCP\n * servers/tools. Fail-closed on missing tenant.\n */\n\nimport { z } from \"zod\";\n\nimport { adaptMcpTool, type McpClientLike, type ToolDefinition, type ToolRegistry } from \"./tools\";\n\n/**\n * Port over an MCP server catalog (satisfied by `@nebutra/mcp`'s\n * `serverRegistry` + plan middleware). Returns only the tools visible to the\n * given tenant/plan — visibility/plan-gating is the port's responsibility.\n */\nexport interface McpServerCatalogPort {\n listTools(ctx: {\n tenantId: string;\n plan?: string;\n }): Promise<{ server: string; definition: ToolDefinition }[]>;\n}\n\nconst ctxSchema = z.object({\n tenantId: z.string().trim().min(1, \"tenantId is required (fail-closed)\"),\n plan: z.string().min(1).optional(),\n});\n\nexport interface ActivateMcpToolsResult {\n /** Tool names newly registered into the registry, in catalog order. */\n readonly registered: readonly string[];\n /** Tool names skipped because the name was already registered. */\n readonly skipped: readonly string[];\n}\n\n/**\n * List the tenant/plan-visible MCP tools, adapt each via {@link adaptMcpTool},\n * and register them into the {@link ToolRegistry}. A tool whose name is already\n * registered is skipped (reported, never thrown). Empty/blank tenantId fails\n * closed before any catalog call.\n */\nexport async function activateMcpTools(\n registry: ToolRegistry,\n catalog: McpServerCatalogPort,\n client: McpClientLike,\n ctx: { tenantId: string; plan?: string },\n): Promise<ActivateMcpToolsResult> {\n const scope = ctxSchema.parse(ctx);\n\n const entries = await catalog.listTools(\n scope.plan === undefined\n ? { tenantId: scope.tenantId }\n : { tenantId: scope.tenantId, plan: scope.plan },\n );\n\n const registered: string[] = [];\n const skipped: string[] = [];\n\n for (const { server, definition } of entries) {\n if (registry.list().some((r) => r.definition.name === definition.name)) {\n skipped.push(definition.name);\n continue;\n }\n const adapted = adaptMcpTool(server, definition, client);\n registry.register(adapted.definition, adapted.handler, adapted.origin);\n registered.push(definition.name);\n }\n\n return { registered, skipped };\n}\n"],"mappings":";;;;;AAaA,SAAS,SAAS;AAgBlB,IAAM,YAAY,EAAE,OAAO;AAAA,EACzB,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,oCAAoC;AAAA,EACvE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AACnC,CAAC;AAeD,eAAsB,iBACpB,UACA,SACA,QACA,KACiC;AACjC,QAAM,QAAQ,UAAU,MAAM,GAAG;AAEjC,QAAM,UAAU,MAAM,QAAQ;AAAA,IAC5B,MAAM,SAAS,SACX,EAAE,UAAU,MAAM,SAAS,IAC3B,EAAE,UAAU,MAAM,UAAU,MAAM,MAAM,KAAK;AAAA,EACnD;AAEA,QAAM,aAAuB,CAAC;AAC9B,QAAM,UAAoB,CAAC;AAE3B,aAAW,EAAE,QAAQ,WAAW,KAAK,SAAS;AAC5C,QAAI,SAAS,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,WAAW,SAAS,WAAW,IAAI,GAAG;AACtE,cAAQ,KAAK,WAAW,IAAI;AAC5B;AAAA,IACF;AACA,UAAM,UAAU,aAAa,QAAQ,YAAY,MAAM;AACvD,aAAS,SAAS,QAAQ,YAAY,QAAQ,SAAS,QAAQ,MAAM;AACrE,eAAW,KAAK,WAAW,IAAI;AAAA,EACjC;AAEA,SAAO,EAAE,YAAY,QAAQ;AAC/B;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/skills.ts"],"sourcesContent":["/**\n * Skills system (WRAP — progressive-disclosure capability).\n *\n * Faithful re-expression of the source harness's two-phase skill model:\n *\n * Phase 1 — Listing (cheap, always in context): a budget-bounded catalog\n * exposing only {name, description, whenToUse} per skill. First-party\n * skills (bundled/builtin tier) are never truncated and never dropped;\n * lower-tier skills degrade to names-only, then drop, as the context\n * budget is exhausted. Path-activated skills stay hidden until a touched\n * file matches one of their globs.\n *\n * Phase 2 — Invocation (lazy, on demand): expand the chosen skill's body\n * into turn messages, with arg/var substitution and a merged tool\n * allowlist contributed to the permission context.\n *\n * A skill IS a `Definition` — the same tenant-scoped layered resolver backs\n * the command registry and the subagent registry (see ./definitions).\n *\n * SECURITY: this re-expression intentionally drops the source's body-side\n * command-substitution (the upstream \"!`…`\" inline-shell facility). Skill\n * bodies are inert text; they NEVER trigger host shell execution here. All\n * entry points require a non-empty `tenantId` and fail closed; cross-tenant\n * inputs are rejected by the underlying `DefinitionResolver`. Pure data/logic\n * — no host filesystem access (resource base is an injected indirection).\n */\n\nimport { z } from \"zod\";\nimport {\n type Definition,\n type DefinitionResolver,\n type ResolveContext,\n substituteArguments,\n} from \"./definitions\";\n\n/** A skill is just a resolvable `Definition`. */\nexport type SkillRecord = Definition;\n\n/** Tiers treated as first-party (never truncated, never dropped). */\nconst FIRST_PARTY_TIERS: ReadonlySet<string> = new Set([\"bundled\", \"builtin\"]);\n\nconst isFirstParty = (s: SkillRecord): boolean => FIRST_PARTY_TIERS.has(s.sourceTier);\n\n// ── Phase 1: listing ─────────────────────────────────────────────────────────\n\n/** One catalog entry — only the cheap, always-in-context surface. */\nexport interface SkillListingEntry {\n readonly name: string;\n readonly description: string;\n readonly whenToUse?: string | undefined;\n /** True when this entry was degraded to names-only by the budget. */\n readonly degraded: boolean;\n readonly firstParty: boolean;\n}\n\nexport interface SkillListing {\n readonly entries: readonly SkillListingEntry[];\n readonly text: string;\n}\n\nconst listingOptionsSchema = z.object({\n maxListingDescChars: z.number().int().positive().default(250),\n contextWindowTokens: z.number().int().positive().default(200_000),\n budgetPercent: z.number().positive().max(1).default(0.01),\n touchedPaths: z.array(z.string()).default([]),\n});\nexport type SkillListingOptions = z.input<typeof listingOptionsSchema>;\n\n/** Cheap, deterministic token estimate (≈4 chars/token). */\nconst estimateTokens = (text: string): number => Math.ceil(text.length / 4);\n\n/** Minimal glob: `**` = any chars, `*` = any chars except `/`, else exact. */\nfunction globToRegExp(glob: string): RegExp {\n let out = \"^\";\n for (let i = 0; i < glob.length; i++) {\n const c = glob[i]!;\n if (c === \"*\") {\n if (glob[i + 1] === \"*\") {\n out += \".*\";\n i++;\n } else {\n out += \"[^/]*\";\n }\n } else if (\".+?^${}()|[]\\\\\".includes(c)) {\n out += `\\\\${c}`;\n } else {\n out += c;\n }\n }\n return new RegExp(`${out}$`);\n}\n\nconst pathMatches = (globs: readonly string[], touched: readonly string[]): boolean =>\n globs.some((g) => {\n const re = globToRegExp(g);\n return touched.some((p) => re.test(p));\n });\n\n/** Stable order: first-party first, then by slug — deterministic output. */\nfunction orderSkills(skills: readonly SkillRecord[]): SkillRecord[] {\n return [...skills].sort((a, b) => {\n const fa = isFirstParty(a) ? 0 : 1;\n const fb = isFirstParty(b) ? 0 : 1;\n if (fa !== fb) return fa - fb;\n return a.slug.localeCompare(b.slug);\n });\n}\n\nconst truncate = (s: string, max: number): string => (s.length <= max ? s : s.slice(0, max));\n\nfunction renderEntry(e: SkillListingEntry): string {\n const head = e.whenToUse ? `- ${e.name} — ${e.whenToUse}` : `- ${e.name}`;\n return e.description ? `${head}\\n ${e.description}` : head;\n}\n\n/**\n * Phase 1 — build the always-in-context skill catalog. Pure & deterministic.\n *\n * Budget = `contextWindowTokens * budgetPercent`. First-party skills are\n * emitted in full and do NOT consume budget against being dropped. Each\n * remaining skill is tried at full description; if it would overflow it is\n * degraded to names-only; if even that overflows it is dropped. Skills in\n * `ctx.loadedSlugs` and unactivated path-gated skills are suppressed.\n */\nexport function buildSkillListing(\n resolver: DefinitionResolver<SkillRecord>,\n ctx: ResolveContext,\n options: SkillListingOptions = {},\n): SkillListing {\n if (!ctx.tenantId) throw new Error(\"buildSkillListing: tenantId is required\");\n const opts = listingOptionsSchema.parse(options);\n\n const loaded = ctx.loadedSlugs ?? new Set<string>();\n const visible = resolver\n .resolve(ctx)\n .filter((s) => !loaded.has(s.slug))\n .filter((s) => {\n const paths = s.frontmatter.paths;\n return paths.length === 0 || pathMatches(paths, opts.touchedPaths);\n });\n\n const budget = Math.max(1, Math.floor(opts.contextWindowTokens * opts.budgetPercent));\n\n const entries: SkillListingEntry[] = [];\n let used = 0;\n\n for (const s of orderSkills(visible)) {\n const fp = isFirstParty(s);\n const fm = s.frontmatter;\n const fullDesc = fp ? fm.description : truncate(fm.description, opts.maxListingDescChars);\n\n if (fp) {\n const entry: SkillListingEntry = {\n name: fm.name,\n description: fm.description,\n whenToUse: fm.whenToUse,\n degraded: false,\n firstParty: true,\n };\n entries.push(entry);\n used += estimateTokens(renderEntry(entry));\n continue;\n }\n\n const full: SkillListingEntry = {\n name: fm.name,\n description: fullDesc,\n whenToUse: fm.whenToUse,\n degraded: false,\n firstParty: false,\n };\n const fullCost = estimateTokens(renderEntry(full));\n if (used + fullCost <= budget) {\n entries.push(full);\n used += fullCost;\n continue;\n }\n\n const nameOnly: SkillListingEntry = {\n name: fm.name,\n description: \"\",\n whenToUse: undefined,\n degraded: true,\n firstParty: false,\n };\n const nameCost = estimateTokens(renderEntry(nameOnly));\n if (used + nameCost <= budget) {\n entries.push(nameOnly);\n used += nameCost;\n }\n // else: drop entirely.\n }\n\n const text = entries.map(renderEntry).join(\"\\n\");\n return { entries, text };\n}\n\n// ── Phase 2: invocation ──────────────────────────────────────────────────────\n\nexport interface SkillMessage {\n readonly role: \"system\" | \"user\";\n readonly content: string;\n}\n\nexport interface ExpandedSkill {\n readonly messages: readonly SkillMessage[];\n readonly allowedTools: readonly string[];\n readonly modelOverride?: string | undefined;\n readonly effort?: \"low\" | \"medium\" | \"high\" | undefined;\n readonly executionMode: \"inline\" | \"fork\";\n}\n\nexport interface ExpandContext {\n readonly tenantId: string;\n readonly args: Record<string, string>;\n readonly vars: Record<string, string>;\n /** Lazy body fetch — host supplies; called only here, only once. */\n readonly bodyLoader: (bodyRef: string) => Promise<string>;\n /** Host-injected resource-base indirection (NO host FS access here). */\n readonly skillResourceBase: (slug: string) => string;\n}\n\n/**\n * Phase 2 — expand a skill into turn messages. Body is loaded lazily via the\n * injected `bodyLoader` (never during listing). A `Base directory…` line is\n * prepended via the injected `skillResourceBase` indirection. Args + vars are\n * substituted with `substituteArguments`. The skill's `allowedTools` are\n * returned for the caller to merge into the permission context — this never\n * mutates global state and never executes anything from the body.\n */\nexport async function expandSkill(skill: SkillRecord, ctx: ExpandContext): Promise<ExpandedSkill> {\n if (!ctx.tenantId) throw new Error(\"expandSkill: tenantId is required\");\n if (skill.tenantId !== ctx.tenantId) {\n throw new Error(\"expandSkill: cross-tenant skill access rejected\");\n }\n\n const rawBody = await ctx.bodyLoader(skill.bodyRef);\n const body = substituteArguments(rawBody, ctx.args, ctx.vars);\n const baseLine = `Base directory for this skill: ${ctx.skillResourceBase(skill.slug)}`;\n\n const messages: readonly SkillMessage[] = [\n { role: \"system\", content: baseLine },\n { role: \"user\", content: body },\n ];\n\n const allowedTools = [...skill.frontmatter.allowedTools];\n\n return {\n messages,\n allowedTools,\n modelOverride: skill.frontmatter.model,\n effort: skill.frontmatter.effort,\n executionMode: skill.frontmatter.executionMode,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Web-standard transport binding tests — no server, no framework.
|
|
3
|
-
*
|
|
4
|
-
* Every test constructs a `new Request(...)` and inspects a `Response` via
|
|
5
|
-
* Web-standard APIs only (`response.text()`, `response.body!.getReader()`).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, expect, it } from "vitest";
|
|
9
|
-
import { ProtocolDispatcher } from "../dispatcher";
|
|
10
|
-
import type { ThreadEvent } from "../model";
|
|
11
|
-
import { createRpcHandler, sseResponse, subscribeNotifications } from "./dispatcher-sse.js";
|
|
12
|
-
|
|
13
|
-
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
/** A dispatcher with one echo handler bound to a real registered method. */
|
|
16
|
-
function freshDispatcher(): ProtocolDispatcher {
|
|
17
|
-
const d = new ProtocolDispatcher();
|
|
18
|
-
// `thread/start` is in the method registry; echo its params back.
|
|
19
|
-
d.register("thread/start", async (params) => ({ echoed: params }));
|
|
20
|
-
return d;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function jsonRequest(body: unknown): Request {
|
|
24
|
-
return new Request("https://edge.test/rpc", {
|
|
25
|
-
method: "POST",
|
|
26
|
-
headers: { "content-type": "application/json" },
|
|
27
|
-
body: typeof body === "string" ? body : JSON.stringify(body),
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Drain an SSE Response body into its raw decoded text. */
|
|
32
|
-
async function drainSse(res: Response): Promise<string> {
|
|
33
|
-
if (!res.body) throw new Error("SSE response body is missing");
|
|
34
|
-
const reader = res.body.getReader();
|
|
35
|
-
const decoder = new TextDecoder();
|
|
36
|
-
let out = "";
|
|
37
|
-
for (;;) {
|
|
38
|
-
const { value, done } = await reader.read();
|
|
39
|
-
if (done) break;
|
|
40
|
-
out += decoder.decode(value, { stream: true });
|
|
41
|
-
}
|
|
42
|
-
out += decoder.decode();
|
|
43
|
-
return out;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function* eventsOf(...events: ThreadEvent[]): AsyncIterable<ThreadEvent> {
|
|
47
|
-
for (const e of events) yield e;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ── createRpcHandler ─────────────────────────────────────────────────────────
|
|
51
|
-
|
|
52
|
-
describe("createRpcHandler", () => {
|
|
53
|
-
it("returns a JSON-RPC success envelope (200, application/json) on happy path", async () => {
|
|
54
|
-
const handler = createRpcHandler(freshDispatcher());
|
|
55
|
-
const res = await handler(
|
|
56
|
-
jsonRequest({
|
|
57
|
-
jsonrpc: "2.0",
|
|
58
|
-
id: 1,
|
|
59
|
-
method: "thread/start",
|
|
60
|
-
tenantId: "org_123",
|
|
61
|
-
params: { hello: "world" },
|
|
62
|
-
}),
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
expect(res.status).toBe(200);
|
|
66
|
-
expect(res.headers.get("content-type")).toContain("application/json");
|
|
67
|
-
const body = await res.json();
|
|
68
|
-
expect(body).toEqual({
|
|
69
|
-
jsonrpc: "2.0",
|
|
70
|
-
id: 1,
|
|
71
|
-
result: { echoed: { hello: "world" } },
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("returns a well-formed JSON-RPC parse error (400) on malformed JSON — never throws", async () => {
|
|
76
|
-
const handler = createRpcHandler(freshDispatcher());
|
|
77
|
-
const res = await handler(jsonRequest("{ not valid json"));
|
|
78
|
-
|
|
79
|
-
expect(res.status).toBe(400);
|
|
80
|
-
expect(res.headers.get("content-type")).toContain("application/json");
|
|
81
|
-
const body = await res.json();
|
|
82
|
-
expect(body.jsonrpc).toBe("2.0");
|
|
83
|
-
expect(body.id).toBeNull();
|
|
84
|
-
expect(body.error.code).toBe(-32700);
|
|
85
|
-
expect(typeof body.error.message).toBe("string");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("fails closed on a missing-tenant envelope (200 JSON-RPC business error)", async () => {
|
|
89
|
-
const handler = createRpcHandler(freshDispatcher());
|
|
90
|
-
const res = await handler(
|
|
91
|
-
jsonRequest({ jsonrpc: "2.0", id: 7, method: "thread/start", params: {} }),
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
// Envelope was parseable JSON → 200, but dispatch fails closed.
|
|
95
|
-
expect(res.status).toBe(200);
|
|
96
|
-
const body = await res.json();
|
|
97
|
-
expect(body.jsonrpc).toBe("2.0");
|
|
98
|
-
expect(body.error).toBeDefined();
|
|
99
|
-
expect(body.error.code).toBe(-32600);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("returns method-not-found JSON-RPC error for an unknown method", async () => {
|
|
103
|
-
const handler = createRpcHandler(freshDispatcher());
|
|
104
|
-
const res = await handler(
|
|
105
|
-
jsonRequest({ jsonrpc: "2.0", id: 9, method: "totally/unknown", tenantId: "org_1" }),
|
|
106
|
-
);
|
|
107
|
-
expect(res.status).toBe(200);
|
|
108
|
-
const body = await res.json();
|
|
109
|
-
expect(body.error.code).toBe(-32601);
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// ── sseResponse ──────────────────────────────────────────────────────────────
|
|
114
|
-
|
|
115
|
-
describe("sseResponse", () => {
|
|
116
|
-
it("sets text/event-stream + no-cache headers", () => {
|
|
117
|
-
const res = sseResponse(eventsOf({ type: "turn.started" }));
|
|
118
|
-
expect(res.status).toBe(200);
|
|
119
|
-
expect(res.headers.get("content-type")).toContain("text/event-stream");
|
|
120
|
-
expect(res.headers.get("cache-control")).toContain("no-cache");
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("streams multiple events in order with correct SSE framing", async () => {
|
|
124
|
-
const stream = eventsOf(
|
|
125
|
-
{ type: "thread.started", threadId: "t1" },
|
|
126
|
-
{ type: "turn.started" },
|
|
127
|
-
{
|
|
128
|
-
type: "turn.completed",
|
|
129
|
-
usage: { inputTokens: 1, cachedInputTokens: 0, outputTokens: 2, reasoningOutputTokens: 0 },
|
|
130
|
-
},
|
|
131
|
-
);
|
|
132
|
-
const text = await drainSse(sseResponse(stream));
|
|
133
|
-
|
|
134
|
-
const frames = text.split("\n\n").filter(Boolean);
|
|
135
|
-
expect(frames).toHaveLength(3);
|
|
136
|
-
expect(frames[0]).toBe(
|
|
137
|
-
`event: thread.started\ndata: ${JSON.stringify({ type: "thread.started", threadId: "t1" })}`,
|
|
138
|
-
);
|
|
139
|
-
expect(frames[1]).toBe(
|
|
140
|
-
`event: turn.started\ndata: ${JSON.stringify({ type: "turn.started" })}`,
|
|
141
|
-
);
|
|
142
|
-
expect((frames[2] ?? "").startsWith("event: turn.completed\n")).toBe(true);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("surfaces a mid-stream iterable error as a final `event: error` frame then closes", async () => {
|
|
146
|
-
async function* boom(): AsyncIterable<ThreadEvent> {
|
|
147
|
-
yield { type: "turn.started" };
|
|
148
|
-
throw new Error("upstream exploded");
|
|
149
|
-
}
|
|
150
|
-
const text = await drainSse(sseResponse(boom()));
|
|
151
|
-
const frames = text.split("\n\n").filter(Boolean);
|
|
152
|
-
|
|
153
|
-
expect(frames[0]).toBe(
|
|
154
|
-
`event: turn.started\ndata: ${JSON.stringify({ type: "turn.started" })}`,
|
|
155
|
-
);
|
|
156
|
-
const last = frames.at(-1) ?? "";
|
|
157
|
-
expect(last.startsWith("event: error\n")).toBe(true);
|
|
158
|
-
expect(last).toContain("upstream exploded");
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("cancels promptly when the provided AbortSignal aborts", async () => {
|
|
162
|
-
const ac = new AbortController();
|
|
163
|
-
let produced = 0;
|
|
164
|
-
async function* infinite(): AsyncIterable<ThreadEvent> {
|
|
165
|
-
for (;;) {
|
|
166
|
-
produced += 1;
|
|
167
|
-
yield { type: "turn.started" };
|
|
168
|
-
if (produced === 2) ac.abort();
|
|
169
|
-
await new Promise((r) => setTimeout(r, 1));
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
const res = sseResponse(infinite(), { signal: ac.signal });
|
|
173
|
-
const text = await drainSse(res);
|
|
174
|
-
// Stream terminated; did not run forever.
|
|
175
|
-
expect(text.length).toBeGreaterThan(0);
|
|
176
|
-
expect(produced).toBeLessThan(50);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it("does not throw when aborted before the first read", async () => {
|
|
180
|
-
const ac = new AbortController();
|
|
181
|
-
ac.abort();
|
|
182
|
-
const res = sseResponse(eventsOf({ type: "turn.started" }), { signal: ac.signal });
|
|
183
|
-
const text = await drainSse(res);
|
|
184
|
-
expect(typeof text).toBe("string");
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
// ── subscribeNotifications ───────────────────────────────────────────────────
|
|
189
|
-
|
|
190
|
-
describe("subscribeNotifications", () => {
|
|
191
|
-
it("bridges dispatcher notifications into an async iterable", async () => {
|
|
192
|
-
const d = new ProtocolDispatcher();
|
|
193
|
-
const iter = subscribeNotifications(d, ["turn/started", "turn/completed"]);
|
|
194
|
-
const reader = iter[Symbol.asyncIterator]();
|
|
195
|
-
|
|
196
|
-
queueMicrotask(() => {
|
|
197
|
-
d.emitNotification("turn/started", { a: 1 });
|
|
198
|
-
d.emitNotification("turn/completed", { b: 2 });
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
const first = await reader.next();
|
|
202
|
-
const second = await reader.next();
|
|
203
|
-
expect(first.value).toEqual({ name: "turn/started", payload: { a: 1 } });
|
|
204
|
-
expect(second.value).toEqual({ name: "turn/completed", payload: { b: 2 } });
|
|
205
|
-
|
|
206
|
-
await reader.return?.();
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it("stops yielding after return() unsubscribes", async () => {
|
|
210
|
-
const d = new ProtocolDispatcher();
|
|
211
|
-
const iter = subscribeNotifications(d, ["turn/started"]);
|
|
212
|
-
const reader = iter[Symbol.asyncIterator]();
|
|
213
|
-
const done = await reader.return?.();
|
|
214
|
-
expect(done?.done).toBe(true);
|
|
215
|
-
// Emitting after teardown must not throw or buffer indefinitely.
|
|
216
|
-
expect(() => d.emitNotification("turn/started", {})).not.toThrow();
|
|
217
|
-
});
|
|
218
|
-
});
|