@juno-ai/bind 9.0.0 → 11.0.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/README.md +375 -15
- package/contracts/index.d.ts +1 -1
- package/contracts/index.js +1 -1
- package/contracts/turn.d.ts +77 -2
- package/contracts/turn.js +35 -2
- package/index.d.ts +6 -2
- package/index.js +6 -2
- package/loop/index.d.ts +2 -1
- package/loop/index.js +1 -1
- package/loop/tool-loop.d.ts +117 -12
- package/loop/tool-loop.js +242 -67
- package/package.json +10 -2
- package/plugins/dispatch.d.ts +130 -0
- package/plugins/dispatch.js +241 -0
- package/plugins/index.d.ts +2 -0
- package/plugins/index.js +2 -0
- package/plugins/tool-message.d.ts +23 -0
- package/plugins/tool-message.js +31 -0
- package/skills/activation.d.ts +64 -0
- package/skills/activation.js +39 -0
- package/skills/admission.d.ts +61 -0
- package/skills/admission.js +41 -0
- package/skills/catalog.d.ts +54 -0
- package/skills/catalog.js +77 -0
- package/skills/discovery.d.ts +82 -0
- package/skills/discovery.js +91 -0
- package/skills/index.d.ts +19 -0
- package/skills/index.js +19 -0
- package/skills/refs.d.ts +21 -0
- package/skills/refs.js +27 -0
- package/skills/registry.d.ts +57 -0
- package/skills/registry.js +94 -0
- package/skills/resolve.d.ts +89 -0
- package/skills/resolve.js +124 -0
- package/skills/sha.d.ts +53 -0
- package/skills/sha.js +60 -0
- package/skills/sha256.d.ts +38 -0
- package/skills/sha256.js +122 -0
- package/skills/skill-md.d.ts +73 -0
- package/skills/skill-md.js +149 -0
- package/skills/types.d.ts +174 -0
- package/skills/types.js +55 -0
- package/testing/index.d.ts +153 -0
- package/testing/index.js +188 -0
- package/tools/control-chars.d.ts +23 -0
- package/tools/control-chars.js +35 -0
package/README.md
CHANGED
|
@@ -35,6 +35,85 @@ constraints that will fail CI if you break them.
|
|
|
35
35
|
|
|
36
36
|
**Added**
|
|
37
37
|
|
|
38
|
+
- **`@juno-ai/bind/skills`** — progressive disclosure for *instructions*, the
|
|
39
|
+
mirror of `@juno-ai/bind/plugins`. `createSkillRegistry` for what your source
|
|
40
|
+
ships, `partitionSkillCatalog` for the Tier-1 catalog and its token budget,
|
|
41
|
+
`resolveActiveSkillInstructions` for the bodies (live-head or pinned to a
|
|
42
|
+
completed run's hashes, over a batched `ExternalSkillSource` for skills your
|
|
43
|
+
users author), `createSkillActivation` to drive the loop's `activateSkills`
|
|
44
|
+
port, `admitSkillLoad` for the active-set bounds, `parseSkillMarkdown` /
|
|
45
|
+
`serializeSkillMarkdown` for the `SKILL.md` interchange format over your own
|
|
46
|
+
YAML, and `buildAgentSkillsDiscoveryIndex` for the Agent Skills Discovery RFC
|
|
47
|
+
v0.2.0 document. See "How to give an agent loadable skills".
|
|
48
|
+
|
|
49
|
+
Two notes for a host that already has something like this. The catalog
|
|
50
|
+
returns **data, not prose** — the wording is yours, for the same prompt-cache
|
|
51
|
+
reason `partitionPluginCatalog` gives. And the content digest is **the
|
|
52
|
+
package's**, not a port like the one `toolCallArgsHash` takes: a skill's hash
|
|
53
|
+
is computed at registration, which is synchronous, and it identifies build
|
|
54
|
+
content rather than being persisted across versions. `Sha256Hex` is still a
|
|
55
|
+
parameter if you would rather inject a native one.
|
|
56
|
+
|
|
57
|
+
**Breaking**
|
|
58
|
+
|
|
59
|
+
- `runToolLoop` now returns `ToolLoopResult` (`{ stopReason, stats }`) instead
|
|
60
|
+
of `void`. A caller that ignores the return value is unchanged, but a
|
|
61
|
+
wrapper *annotated* `Promise<void>` no longer typechecks — widen it to
|
|
62
|
+
`Promise<ToolLoopResult>`.
|
|
63
|
+
- `RunStats` gained `cachedInputTokens`. `emptyRunStats()` and the folds set it;
|
|
64
|
+
code that hand-builds a `RunStats` literal must add the field. `ToolLoopTurn`
|
|
65
|
+
and `CompactionApplied` gained a matching optional `cachedInputTokens`, so
|
|
66
|
+
the loop can actually populate it — return it from `callModel` if your
|
|
67
|
+
transport reports one.
|
|
68
|
+
- `MissingActivationPortError` — a tool outcome that asks the loop to activate
|
|
69
|
+
a plugin or skill while the matching port is unwired is now reported through
|
|
70
|
+
`onToolCallRejected` instead of being dropped in silence. The run still
|
|
71
|
+
completes; the wiring bug is no longer invisible.
|
|
72
|
+
- `defineTool` no longer applies `normalizeArgs` inside `execute`.
|
|
73
|
+
Normalization is the dispatcher's step (it runs before the idempotency
|
|
74
|
+
hash), and applying it in both places applied it twice.
|
|
75
|
+
- **`StopReason` no longer has `suspended`.** It split into
|
|
76
|
+
`waiting_for_reply` (a tool asked a human a question; nothing happens until
|
|
77
|
+
someone answers) and `resuming_later` (a tool scheduled its own resume).
|
|
78
|
+
Removing a union member breaks any exhaustive `switch`, so map both new
|
|
79
|
+
values wherever you handled `suspended` — and note that the old single value
|
|
80
|
+
could not tell them apart at all, which is why it was split.
|
|
81
|
+
- **`@juno-ai/bind/plugins` now loads `zod` at runtime.** The barrel re-exports
|
|
82
|
+
`defineTool` / `pluginFromTools`, and `toolWireDefinition` needs
|
|
83
|
+
`z.toJSONSchema`. Every other module in the subpath was previously
|
|
84
|
+
runtime-zod-free, so a consumer importing only `createToolRegistry` while
|
|
85
|
+
ignoring the peer-dependency warning now fails to resolve. `zod` is a
|
|
86
|
+
required (non-optional) peer, so a correctly-installed consumer is
|
|
87
|
+
unaffected. `@juno-ai/bind/loop` is deliberately still zod-free — which is
|
|
88
|
+
why `toolResultMessage` lives in its own type-only module.
|
|
89
|
+
|
|
90
|
+
**Added**
|
|
91
|
+
|
|
92
|
+
- `ToolLoopResult.stopReason` — `done` / `waiting_for_reply` / `resuming_later`
|
|
93
|
+
/ `iteration_limit` / `aborted`, one per exit, replacing the two or three
|
|
94
|
+
loop-state flags every host was combining differently. The two pause reasons
|
|
95
|
+
are separate values on purpose: one needs a person to act and the other does
|
|
96
|
+
not, and that is the distinction a host most needs to surface. `deadline` stays in the `StopReason` union
|
|
97
|
+
for a host classifying a thrown `RunTimeoutError`; the loop cannot return it,
|
|
98
|
+
and the type's doc comment says why.
|
|
99
|
+
- `ToolLoopResult.stats` — a `RunStats` the loop folds itself: turns,
|
|
100
|
+
dispatched tool calls, tokens, cost, and the model-time/tool-time split with
|
|
101
|
+
a per-tool breakdown. A compaction contributes its spend but not a turn
|
|
102
|
+
(`accumulateAuxiliarySpend`, also new), so `stats.turns` stays comparable
|
|
103
|
+
with `maxIterations`. `ToolLoopParams.now` injects the clock.
|
|
104
|
+
- `@juno-ai/bind/testing` — the scripted-model fixtures this package's own
|
|
105
|
+
cross-module suites use: `loopHarness`, `scriptedModel`, `toolCallTurn`,
|
|
106
|
+
`finalAnswer`, `toolCall`, `freshState`, `recordingSink`, `steppingClock`.
|
|
107
|
+
Credential-free multi-turn, multi-tool tests without mocking a chat client.
|
|
108
|
+
- `defineTool` / `pluginFromTools` (`@juno-ai/bind/plugins`) — author a tool as
|
|
109
|
+
`{ name, description, schema, execute }` and get argument parsing, a
|
|
110
|
+
`validation` failure the model can act on, and typed `args` in `execute`.
|
|
111
|
+
Plus `toolWireDefinition` (zod → sanitized JSON Schema) and
|
|
112
|
+
`toolResultMessage` (the `role:"tool"` encoding the loop itself uses).
|
|
113
|
+
- `ToolLoopParams.activePlugins`, `activatePlugins` and `activateSkills` are
|
|
114
|
+
now optional. A host with a fixed tool surface had been required to supply
|
|
115
|
+
empty functions; `activePlugins` was never read by the loop at all.
|
|
116
|
+
|
|
38
117
|
- `runToolCallsPooledByTool` accepts an optional `signal`, and
|
|
39
118
|
`AbortedToolCallError` / `ToolBatchOptions` are exported from
|
|
40
119
|
`@juno-ai/bind/run`. Additive — a caller that passes nothing is unchanged, and
|
|
@@ -297,26 +376,165 @@ const state: ToolLoopState = {
|
|
|
297
376
|
toolCalls: 0,
|
|
298
377
|
};
|
|
299
378
|
|
|
300
|
-
await runToolLoop({
|
|
379
|
+
const { stopReason, stats } = await runToolLoop({
|
|
301
380
|
state,
|
|
302
|
-
activePlugins,
|
|
303
381
|
maxIterations: 30,
|
|
304
382
|
|
|
305
383
|
callModel: (messages, tools) => llm.complete({ messages, tools }),
|
|
306
384
|
buildTools: () => registry.toolDefinitions([...activePlugins]),
|
|
307
385
|
runToolCall: (call) => dispatch(call),
|
|
386
|
+
|
|
387
|
+
// Only if your tools can change the tool surface mid-run:
|
|
308
388
|
activatePlugins: (names) => names.forEach((n) => activePlugins.add(n)),
|
|
309
389
|
activateSkills: (refs) => loadInstructions(refs),
|
|
310
390
|
});
|
|
311
391
|
|
|
312
|
-
// `state` is mutated in place — read totals off it
|
|
313
|
-
// heartbeat.
|
|
392
|
+
// `state` is mutated in place — read totals off it mid-run from a heartbeat.
|
|
314
393
|
console.log(state.inputTokens, state.outputTokens, state.toolCalls);
|
|
394
|
+
|
|
395
|
+
// The result is the run's conclusion, which only exists once it is over.
|
|
396
|
+
await persistRun({ stopReason, ...stats });
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
The two halves are deliberate. `state` is mutated rather than returned so a
|
|
400
|
+
heartbeat can read live totals while the run is still going; `ToolLoopResult`
|
|
401
|
+
is what could not exist until the run ended.
|
|
402
|
+
|
|
403
|
+
`stopReason` is the whole outcome, in one word:
|
|
404
|
+
|
|
405
|
+
| Reason | The model | What a host should say |
|
|
406
|
+
|---|---|---|
|
|
407
|
+
| `done` | Stopped calling tools | It answered |
|
|
408
|
+
| `waiting_for_reply` | A tool asked a human a question | It needs you to answer — nothing happens until you do |
|
|
409
|
+
| `resuming_later` | A tool scheduled its own resume | It paused on purpose and will come back |
|
|
410
|
+
| `iteration_limit` | Still calling tools at the ceiling | It ran out of room, mid-task |
|
|
411
|
+
| `aborted` | Cut off by `shouldStop` or the batch `signal` | It was stopped |
|
|
412
|
+
|
|
413
|
+
The two pause reasons are separate values because their consequences differ
|
|
414
|
+
more than any other pair: one needs a person to act, the other needs nobody to.
|
|
415
|
+
Collapsed into one `suspended`, a host that wanted to say which had to go back
|
|
416
|
+
and read `state.suspended` — the loop-state-flag reconstruction this return
|
|
417
|
+
value exists to replace.
|
|
418
|
+
|
|
419
|
+
`deadline` is in the `StopReason` union but never returned: the wall-clock port
|
|
420
|
+
(`throwIfTimedOut`) is throw-based, so an expired budget leaves the loop as a
|
|
421
|
+
`RunTimeoutError` your catch block maps — `classifyRunFailure` recognises the
|
|
422
|
+
same condition. The loop returns `aborted` for a fired signal because once a
|
|
423
|
+
deadline and a cancellation are combined into one `AbortSignal` it genuinely
|
|
424
|
+
cannot tell which one fired.
|
|
425
|
+
|
|
426
|
+
`stats` is a `RunStats`: turns, dispatched tool calls, tokens (including
|
|
427
|
+
provider-reported cached input, when your `callModel` returns a
|
|
428
|
+
`cachedInputTokens`), cost, and model time vs tool time with a per-tool
|
|
429
|
+
breakdown. Three things are worth reading carefully:
|
|
430
|
+
|
|
431
|
+
- **`stats` is this invocation's contribution; `state` is whatever you seeded
|
|
432
|
+
plus that.** They match only if you seeded zeros. A host resuming a run seeds
|
|
433
|
+
`state` from the stored totals, and then `state.costCents` is the run's
|
|
434
|
+
lifetime cost while `stats.costCents` is this leg's — bill from whichever you
|
|
435
|
+
mean, and don't substitute one for the other.
|
|
436
|
+
|
|
437
|
+
- **`stats.toolCalls` counts calls that *ran*; `state.toolCalls` counts calls
|
|
438
|
+
the model *requested*.** They differ by exactly the work an aborted batch
|
|
439
|
+
prevented, which is why they are two numbers.
|
|
440
|
+
- **A compaction contributes tokens, cost and model time but not a turn**, so
|
|
441
|
+
`stats.turns` stays comparable with `maxIterations`.
|
|
442
|
+
|
|
443
|
+
Both are lost if the loop throws: a deadline, a cancellation, or a fatal tool
|
|
444
|
+
error leaves no return value, so `state` — mutated in place — is the only
|
|
445
|
+
accounting that survives those exits.
|
|
446
|
+
|
|
447
|
+
Pass `now` to make either measurement deterministic in a test; it defaults to
|
|
448
|
+
`Date.now`.
|
|
449
|
+
|
|
450
|
+
### How to test an agent without a provider
|
|
451
|
+
|
|
452
|
+
`@juno-ai/bind/testing` ships the fixtures this package's own cross-module
|
|
453
|
+
suites use. A scripted model is a queue of prepared turns, so a multi-turn,
|
|
454
|
+
multi-tool test needs no credential, no network, and no mocked chat client.
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
import { runToolLoop } from "@juno-ai/bind/loop";
|
|
458
|
+
import {
|
|
459
|
+
loopHarness, toolCall, toolCallTurn, finalAnswer,
|
|
460
|
+
} from "@juno-ai/bind/testing";
|
|
461
|
+
|
|
462
|
+
const h = loopHarness([
|
|
463
|
+
toolCallTurn([toolCall("search", { q: "bind" }), toolCall("read_file")]),
|
|
464
|
+
toolCallTurn([toolCall("write_file", { path: "out.md" })]),
|
|
465
|
+
finalAnswer("Done."),
|
|
466
|
+
]);
|
|
467
|
+
|
|
468
|
+
const { stopReason, stats } = await runToolLoop(h.params);
|
|
469
|
+
|
|
470
|
+
expect(stopReason).toBe("done");
|
|
471
|
+
expect(stats.turns).toBe(3);
|
|
472
|
+
expect(h.ran).toEqual(["search", "read_file", "write_file"]);
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Every field of `h.params` is overridable, which is how you reach the
|
|
476
|
+
interesting states — `{ runToolCall }` to make one tool fail, `{ signal }` to
|
|
477
|
+
abort mid-batch, `{ now }` to make the timing figures deterministic
|
|
478
|
+
(`steppingClock()` for a fixed tick, or your own closure advanced inside
|
|
479
|
+
`runToolCall` when you want to *choose* each interval).
|
|
480
|
+
`h.ran` records what the loop *dispatched* and `h.sideEffects` what actually
|
|
481
|
+
*completed*; the gap between them is the answer to every cancellation question.
|
|
482
|
+
|
|
483
|
+
Two failure modes are deliberate. A script that runs out throws rather than
|
|
484
|
+
returning an empty turn — otherwise `maxIterations` absorbs the mistake and the
|
|
485
|
+
test passes while describing a run that never happened. And a duplicate
|
|
486
|
+
tool-call id throws at construction, rather than producing a transcript the
|
|
487
|
+
provider rejects ten frames deep in the loop.
|
|
488
|
+
|
|
489
|
+
### How to author a tool without writing dispatch by hand
|
|
490
|
+
|
|
491
|
+
A `ToolPlugin` dispatches by tool name, which is right for a bundle with shared
|
|
492
|
+
setup and pure ceremony for a flat list of independent tools. `defineTool` does
|
|
493
|
+
the mechanical half — parse the arguments, turn a parse failure into something
|
|
494
|
+
the model can act on — and `pluginFromTools` bundles the result.
|
|
495
|
+
|
|
496
|
+
```ts
|
|
497
|
+
import { defineTool, pluginFromTools } from "@juno-ai/bind/plugins";
|
|
498
|
+
|
|
499
|
+
const search = defineTool({
|
|
500
|
+
name: "search",
|
|
501
|
+
description: "Search the corpus.",
|
|
502
|
+
schema: z.object({ query: z.string().min(1), limit: z.number().default(10) }),
|
|
503
|
+
// `args` is typed from the schema; `limit` has already defaulted.
|
|
504
|
+
execute: async (args, ctx: Ctx) => ({
|
|
505
|
+
success: true,
|
|
506
|
+
data: await corpus.search(args.query, args.limit, ctx.tenantId),
|
|
507
|
+
}),
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const plugin = pluginFromTools<Ctx>({
|
|
511
|
+
name: "corpus",
|
|
512
|
+
description: "Corpus tools.",
|
|
513
|
+
tools: [search],
|
|
514
|
+
});
|
|
315
515
|
```
|
|
316
516
|
|
|
317
|
-
|
|
318
|
-
the
|
|
319
|
-
|
|
517
|
+
The schema is the single source of truth: it is converted to JSON Schema for
|
|
518
|
+
the model *and* used to validate what comes back, so the two cannot drift.
|
|
519
|
+
|
|
520
|
+
Bad arguments come back as `{ success: false, kind: "validation", error }`
|
|
521
|
+
naming the offending path — **returned, not thrown**. That distinction is the
|
|
522
|
+
bug this replaces: a thrown parse error turns a recoverable "you passed the
|
|
523
|
+
wrong field" into a dead run. An unknown tool name is likewise a returned
|
|
524
|
+
`not_found`, because a resumed session's history can reference a tool you have
|
|
525
|
+
since retired.
|
|
526
|
+
|
|
527
|
+
The validation lives on the tool, not on the bundle, so a host with its own
|
|
528
|
+
dispatcher can call `search.execute(rawArgs, ctx)` directly and get the same
|
|
529
|
+
guarantee — `pluginFromTools` only resolves names.
|
|
530
|
+
|
|
531
|
+
Two encoders come with it. `toolWireDefinition(tool, wireName?)` converts a
|
|
532
|
+
tool to what the provider is shown, through `sanitizeToolSchema` — `wireName`
|
|
533
|
+
because tool naming is host policy (Monad encodes `plugin__tool` to route a
|
|
534
|
+
call back to its plugin). `toolResultMessage(callId, result)` produces the
|
|
535
|
+
`role:"tool"` message, using the same encoding the loop synthesizes for a
|
|
536
|
+
failed or refused call — so a model never has to learn two error formats in one
|
|
537
|
+
transcript.
|
|
320
538
|
|
|
321
539
|
### How to make a tool take effect mid-batch
|
|
322
540
|
|
|
@@ -876,6 +1094,130 @@ data migration.
|
|
|
876
1094
|
system prompt's business, and re-rendering it byte-identically for unchanged
|
|
877
1095
|
inputs is what preserves a provider's prompt-cache prefix.
|
|
878
1096
|
|
|
1097
|
+
### How to give an agent loadable skills
|
|
1098
|
+
|
|
1099
|
+
A *skill* is a markdown procedure or reference module the model can pull into
|
|
1100
|
+
its own instructions: a catalog line it always sees, a body injected into the
|
|
1101
|
+
system prompt once loaded, and resources it may read after that. It is the same
|
|
1102
|
+
progressive disclosure as tool activation, applied to knowledge — and it exists
|
|
1103
|
+
because the accumulated know-how of a real workspace does not fit in a context
|
|
1104
|
+
window, while a catalog line costs about fifty tokens.
|
|
1105
|
+
|
|
1106
|
+
Register what your own source ships:
|
|
1107
|
+
|
|
1108
|
+
```ts
|
|
1109
|
+
import { createSkillRegistry, partitionSkillCatalog } from "@juno-ai/bind/skills";
|
|
1110
|
+
|
|
1111
|
+
const skills = createSkillRegistry({ onWarn: (message, fields) => log.warn(message, fields) });
|
|
1112
|
+
|
|
1113
|
+
skills.registerPlatform({
|
|
1114
|
+
name: "triaging-inbound-work",
|
|
1115
|
+
description: "How this team triages inbound requests.",
|
|
1116
|
+
whenToUse: "When asked to sort, rank, or route a queue of incoming work.",
|
|
1117
|
+
render: () => TRIAGE_BODY,
|
|
1118
|
+
});
|
|
1119
|
+
// A skill contributed by a plugin is offered only to an agent that can load
|
|
1120
|
+
// that plugin — a recipe for tools it cannot call is noise.
|
|
1121
|
+
skills.registerPlugin("documents", DRAFTING_SKILL);
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
Render the catalog from a partition, in your own words:
|
|
1125
|
+
|
|
1126
|
+
```ts
|
|
1127
|
+
const active = new Set<string>(session.activeSkills);
|
|
1128
|
+
const available = skills.summaries({ availablePlugins: agent.plugins });
|
|
1129
|
+
const { active: loaded, loadable, truncated } = partitionSkillCatalog(active, available);
|
|
1130
|
+
```
|
|
1131
|
+
|
|
1132
|
+
`partitionSkillCatalog` returns **data, not prose**, exactly as
|
|
1133
|
+
`partitionPluginCatalog` does. It also enforces a token budget by marking the
|
|
1134
|
+
overflow `name_only` rather than dropping it — a name is still enough for the
|
|
1135
|
+
model to call `load_skill` and read the real description, whereas a skill it
|
|
1136
|
+
cannot see is one it can never ask for. Pass your own `cost` if your line format
|
|
1137
|
+
differs from `- name: description — whenToUse`; a budget is only as honest as
|
|
1138
|
+
its measurement.
|
|
1139
|
+
|
|
1140
|
+
Then wire activation into the loop, and bound how much can be loaded:
|
|
1141
|
+
|
|
1142
|
+
```ts
|
|
1143
|
+
import {
|
|
1144
|
+
admitSkillLoad,
|
|
1145
|
+
createSkillActivation,
|
|
1146
|
+
estimateSkillBodyTokens,
|
|
1147
|
+
resolveActiveSkillInstructions,
|
|
1148
|
+
} from "@juno-ai/bind/skills";
|
|
1149
|
+
|
|
1150
|
+
const loadedSkillShas: Record<string, string> = {};
|
|
1151
|
+
const resolveActiveInstructions = (activeRefs: string[]) =>
|
|
1152
|
+
resolveActiveSkillInstructions({ activeRefs, available, registry: skills });
|
|
1153
|
+
|
|
1154
|
+
const activation = createSkillActivation({
|
|
1155
|
+
availableSkills: available,
|
|
1156
|
+
activeSkills: active, // yours: seeded before the first turn, persisted after the last
|
|
1157
|
+
loadedSkillShas, // yours: hoist it so a failed run still records what it read
|
|
1158
|
+
store: { resolveActiveInstructions },
|
|
1159
|
+
applyInstructions: (instructions) => renderSystemPrompt({ instructions }),
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
await runToolLoop({ ...params, activateSkills: (refs) => activation.activateSkills(refs) });
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
Two details are worth knowing before you wire your own `load_skill` tool. The
|
|
1166
|
+
resolver renders in a **total order** (origin, then name) rather than the order
|
|
1167
|
+
skills were loaded, because these bodies sit high in the system prompt and a
|
|
1168
|
+
resumed session's persisted order would otherwise byte-shift the cacheable
|
|
1169
|
+
prefix. And `admitSkillLoad` is what keeps a looping model from loading its way
|
|
1170
|
+
into a context-limit error — you measure, it judges:
|
|
1171
|
+
|
|
1172
|
+
```ts
|
|
1173
|
+
// Cheapest first. The count is a `Set.size`; the token bound needs the
|
|
1174
|
+
// resolver, which for a host-stored skill is a query plus the wrapping of every
|
|
1175
|
+
// active body. A model that has hit the cap keeps calling `load_skill`, so
|
|
1176
|
+
// folding these into one pass pays that cost on every call purely to refuse.
|
|
1177
|
+
const byCount = admitSkillLoad({ activeCount: active.size });
|
|
1178
|
+
if (!byCount.admitted) return { success: false, kind: "validation", error: byCount.reason };
|
|
1179
|
+
|
|
1180
|
+
const { instructions } = await resolveActiveInstructions([...active, ref]);
|
|
1181
|
+
const byTokens = admitSkillLoad({ projectedBodyTokens: estimateSkillBodyTokens(instructions) });
|
|
1182
|
+
if (!byTokens.admitted) return { success: false, kind: "validation", error: byTokens.reason };
|
|
1183
|
+
```
|
|
1184
|
+
|
|
1185
|
+
Omitting a measurement omits its bound, which is what makes the two-pass shape
|
|
1186
|
+
expressible — and a measurement that arrives broken (`NaN`, negative) refuses
|
|
1187
|
+
rather than admits, because a nonsense count is not evidence of room.
|
|
1188
|
+
|
|
1189
|
+
Skills your *users* author live in your database, not the registry. Hand the
|
|
1190
|
+
resolver an `externalSource` and it routes any ref that is not `platform:<name>`
|
|
1191
|
+
to you — batched, so one activation stays one query:
|
|
1192
|
+
|
|
1193
|
+
```ts
|
|
1194
|
+
resolveActiveSkillInstructions({
|
|
1195
|
+
activeRefs,
|
|
1196
|
+
available,
|
|
1197
|
+
registry: skills,
|
|
1198
|
+
externalSource: async (refs, pinnedShas) => loadWorkspaceSkills(workspaceId, refs, pinnedShas),
|
|
1199
|
+
});
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
`pinnedShas` is how a replay stays honest. Each resolution records the
|
|
1203
|
+
`contentSha` it actually rendered; feed a completed run's map back in and every
|
|
1204
|
+
skill resolves to the body that run saw, so an eval is not silently grading
|
|
1205
|
+
against instructions that were edited afterwards.
|
|
1206
|
+
|
|
1207
|
+
To read or write the interchange format, pass your own YAML implementation —
|
|
1208
|
+
the package takes peer dependencies only:
|
|
1209
|
+
|
|
1210
|
+
```ts
|
|
1211
|
+
import { parseSkillMarkdown } from "@juno-ai/bind/skills";
|
|
1212
|
+
import yaml from "js-yaml";
|
|
1213
|
+
|
|
1214
|
+
const parsed = parseSkillMarkdown(raw, { parse: yaml.load, stringify: (v) => yaml.dump(v, { lineWidth: -1 }) });
|
|
1215
|
+
```
|
|
1216
|
+
|
|
1217
|
+
Import is deliberately lenient — it repairs the unquoted-colon frontmatter
|
|
1218
|
+
mistake and warns — and fails only on frontmatter that is not YAML and on a
|
|
1219
|
+
missing `description`, the one field with no sensible default.
|
|
1220
|
+
|
|
879
1221
|
### How to restore a persisted activation set
|
|
880
1222
|
|
|
881
1223
|
```ts
|
|
@@ -923,7 +1265,9 @@ client error), call `releaseProbe` so the slot cannot stick.
|
|
|
923
1265
|
your editor. This section covers what the types cannot say: which entry point
|
|
924
1266
|
to reach for, and the contracts that hold between calls.*
|
|
925
1267
|
|
|
926
|
-
Every export is re-exported from the package root
|
|
1268
|
+
Every export is re-exported from the package root **except `@juno-ai/bind/testing`,
|
|
1269
|
+
which is subpath-only so fixtures never reach a production bundle** — but prefer
|
|
1270
|
+
the subpath either way, since it
|
|
927
1271
|
keeps a consumer who only wants routing from pulling in the rest.
|
|
928
1272
|
|
|
929
1273
|
| Import | Owns | Reach for it when |
|
|
@@ -935,7 +1279,9 @@ keeps a consumer who only wants routing from pulling in the rest.
|
|
|
935
1279
|
| `@juno-ai/bind/run` | Wall-clock deadline, failure classification, coalesced heartbeat, tool-batch pooling, child-run lineage and admission, poll backoff, tool-call receipts for retry-safe side effects | A run must be bounded, observable, and able to say *why* it stopped — or it can spawn runs of its own |
|
|
936
1280
|
| `@juno-ai/bind/transcript` | `validateAndHealMessages` | You send transcripts to more than one provider, or you build them across turns |
|
|
937
1281
|
| `@juno-ai/bind/tools` | `sanitizeToolSchema` | Any tool schema reaches a provider — especially third-party ones |
|
|
938
|
-
| `@juno-ai/bind/plugins` | Tool/plugin vocabulary, the registry factory, progressive-disclosure activation | You have more
|
|
1282
|
+
| `@juno-ai/bind/plugins` | Tool/plugin vocabulary, `defineTool` / `pluginFromTools`, the wire-definition and tool-result encoders, the registry factory, progressive-disclosure activation | You are authoring tools, or you have more of them than fit comfortably in one prompt |
|
|
1283
|
+
| `@juno-ai/bind/skills` | Skill vocabulary — the code-skill registry, the `SKILL.md` codec, the content hash, the catalog's total order and token budget, the active-instruction resolver, the activation controller, active-set bounds, and the Agent Skills Discovery document | Your agent needs loadable instructions, not just tools — a workspace's procedures, a house style, a runbook |
|
|
1284
|
+
| `@juno-ai/bind/testing` | Scripted-model fixtures — `loopHarness`, `scriptedModel`, `toolCallTurn`, `finalAnswer`, `freshState`, `recordingSink`, `steppingClock` | You want multi-turn, multi-tool tests without a credential or a mocked chat client |
|
|
939
1285
|
|
|
940
1286
|
### Contracts the types do not carry
|
|
941
1287
|
|
|
@@ -1288,8 +1634,14 @@ const turn: TurnFn = async (messages, tools, signal) => {
|
|
|
1288
1634
|
|
|
1289
1635
|
### Accumulating run statistics
|
|
1290
1636
|
|
|
1637
|
+
**If you use `runToolLoop`, you do not need this** — it folds `RunStats` itself
|
|
1638
|
+
and returns it. This is the shape for a host driving turns by hand.
|
|
1639
|
+
|
|
1291
1640
|
Fold each turn and each tool call as they complete; `RunStats` keeps model time
|
|
1292
|
-
and tool time separate so a slow tool never looks like a slow model.
|
|
1641
|
+
and tool time separate so a slow tool never looks like a slow model. Model
|
|
1642
|
+
calls that are not agent turns — a compaction pass — go through
|
|
1643
|
+
`accumulateAuxiliarySpend` instead, which charges the tokens and the time
|
|
1644
|
+
without counting a turn.
|
|
1293
1645
|
|
|
1294
1646
|
```ts
|
|
1295
1647
|
let stats = emptyRunStats();
|
|
@@ -1401,7 +1753,13 @@ try {
|
|
|
1401
1753
|
stats = accumulateTurn(stats, result);
|
|
1402
1754
|
await heartbeat.beat();
|
|
1403
1755
|
if (!result.message.tool_calls?.length) break;
|
|
1404
|
-
|
|
1756
|
+
const suspend = await runToolBatch(result.message.tool_calls);
|
|
1757
|
+
if (suspend) {
|
|
1758
|
+
// The two pause reasons are separate values — see the stop-reason table.
|
|
1759
|
+
stopReason =
|
|
1760
|
+
suspend.resumeKind === "answer" ? "waiting_for_reply" : "resuming_later";
|
|
1761
|
+
break;
|
|
1762
|
+
}
|
|
1405
1763
|
}
|
|
1406
1764
|
} catch (error) {
|
|
1407
1765
|
stopReason = classifyRunFailure(deadline, error) === "timed_out" ? "deadline" : "aborted";
|
|
@@ -1579,10 +1937,12 @@ two rates weights a 10-token run like a 10,000-token one.
|
|
|
1579
1937
|
Named, not scheduled. Listed so a consumer can tell a deliberate omission from
|
|
1580
1938
|
an oversight.
|
|
1581
1939
|
|
|
1582
|
-
- **
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1940
|
+
- **Time-to-first-token in `RunStats`.** The loop now folds `RunStats` itself,
|
|
1941
|
+
so `modelTimeMs`, `toolTimeMs` and the per-tool breakdown come for free — but
|
|
1942
|
+
`ttftMs` lives on `TurnTimings` and only the transport can see the first byte.
|
|
1943
|
+
A `callModel` that reported its own timings back would close the gap;
|
|
1944
|
+
`ToolLoopTurn` would have to grow, which is a change to the shape every host
|
|
1945
|
+
already implements.
|
|
1586
1946
|
- **A streaming turn contract.** Half of this landed:
|
|
1587
1947
|
`createTurnTextStream` owns the emit/retry interaction for assistant *text*,
|
|
1588
1948
|
arms `producedOutput` itself, and repairs a retractable surface between
|
package/contracts/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { emptyRunStats, accumulateTurn, accumulateToolCall, accumulateRun, type TranscriptMessage, type AssistantTurnMessage, type WireToolDefinition, type WireToolCall, type TurnTimings, type TurnUsage, type ModelTurnResult, type TurnFn, type StopReason, type RunStats, } from "./turn.js";
|
|
1
|
+
export { emptyRunStats, accumulateTurn, accumulateToolCall, accumulateAuxiliarySpend, accumulateRun, type AuxiliarySpend, type TranscriptMessage, type AssistantTurnMessage, type WireToolDefinition, type WireToolCall, type TurnTimings, type TurnUsage, type ModelTurnResult, type TurnFn, type StopReason, type RunStats, } from "./turn.js";
|
package/contracts/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { emptyRunStats, accumulateTurn, accumulateToolCall, accumulateRun, } from "./turn.js";
|
|
1
|
+
export { emptyRunStats, accumulateTurn, accumulateToolCall, accumulateAuxiliarySpend, accumulateRun, } from "./turn.js";
|
package/contracts/turn.d.ts
CHANGED
|
@@ -50,18 +50,67 @@ export interface ModelTurnResult {
|
|
|
50
50
|
* completed assistant turn.
|
|
51
51
|
*/
|
|
52
52
|
export type TurnFn = (messages: readonly TranscriptMessage[], tools: readonly WireToolDefinition[] | undefined, signal: AbortSignal | undefined) => Promise<ModelTurnResult>;
|
|
53
|
-
/**
|
|
54
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Why a run stopped — the single vocabulary a host reports an outcome in,
|
|
55
|
+
* instead of re-deriving it from a handful of loop-state flags.
|
|
56
|
+
*
|
|
57
|
+
* `runToolLoop` returns five of the six directly:
|
|
58
|
+
*
|
|
59
|
+
* - `done` — the model produced a turn with no tool calls (and no
|
|
60
|
+
* `onTurnWouldEnd` nudge pushed it forward).
|
|
61
|
+
* - `waiting_for_reply` — a tool asked a human a question and the run is
|
|
62
|
+
* blocked until someone answers. **Nothing happens until they do.**
|
|
63
|
+
* - `resuming_later` — a tool scheduled its own resume (a sleep, a timer).
|
|
64
|
+
* The run paused on purpose and will come back by itself.
|
|
65
|
+
* - `iteration_limit` — `maxIterations` was exhausted with the model still
|
|
66
|
+
* calling tools. The run did not finish; it was cut off.
|
|
67
|
+
* - `aborted` — the batch `signal` fired, or `shouldStop` asked to stop.
|
|
68
|
+
*
|
|
69
|
+
* The two pause reasons are split rather than one `suspended` because they are
|
|
70
|
+
* the outcomes whose consequences differ most: one needs a person to act, the
|
|
71
|
+
* other needs no one to do anything. Collapsed into a single value, a host
|
|
72
|
+
* that wanted to tell them apart had to go back and read `state.suspended` —
|
|
73
|
+
* which is the loop-state-flag reconstruction this type exists to replace.
|
|
74
|
+
*
|
|
75
|
+
* `deadline` is the one the loop does not return, and deliberately so: the
|
|
76
|
+
* wall-clock budget is a **throw-based** port (`throwIfTimedOut`), so an
|
|
77
|
+
* expired budget leaves the loop as a `RunTimeoutError` rather than a value.
|
|
78
|
+
* A host maps its catch block onto this vocabulary — `classifyRunFailure`
|
|
79
|
+
* from `@juno-ai/bind/run` recognises the same condition as `"timed_out"`.
|
|
80
|
+
* Returning `aborted` for an expired deadline would be worse than not
|
|
81
|
+
* returning it at all: the loop genuinely cannot distinguish a deadline signal
|
|
82
|
+
* from a cancellation signal once both are combined into one `AbortSignal`.
|
|
83
|
+
*/
|
|
84
|
+
export type StopReason = "done" | "waiting_for_reply" | "resuming_later" | "iteration_limit" | "deadline" | "aborted";
|
|
55
85
|
/**
|
|
56
86
|
* Cumulative run accounting: model-time and tool-time reported separately —
|
|
57
87
|
* task wall-clock conflates provider inference speed with tool execution,
|
|
58
88
|
* and consumers comparing models need the model's contribution isolated.
|
|
59
89
|
*/
|
|
60
90
|
export interface RunStats {
|
|
91
|
+
/**
|
|
92
|
+
* Agent turns — one per model completion the loop iterated on. Auxiliary
|
|
93
|
+
* model calls (a compaction pass) contribute their tokens, cost and model
|
|
94
|
+
* time but NOT a turn, so this stays comparable with the loop's iteration
|
|
95
|
+
* budget. See {@link accumulateAuxiliarySpend}.
|
|
96
|
+
*/
|
|
61
97
|
readonly turns: number;
|
|
98
|
+
/**
|
|
99
|
+
* Tool calls that were actually **dispatched**. A call the batch refused at
|
|
100
|
+
* claim time (an aborted `signal`) never ran, so it is not counted here even
|
|
101
|
+
* though the model requested it — the gap between this and the requested
|
|
102
|
+
* count is exactly the work an abort prevented.
|
|
103
|
+
*/
|
|
62
104
|
readonly toolCalls: number;
|
|
63
105
|
readonly inputTokens: number;
|
|
64
106
|
readonly outputTokens: number;
|
|
107
|
+
/**
|
|
108
|
+
* Provider-reported cached input tokens, summed across turns. Zero is
|
|
109
|
+
* indistinguishable from "the transport could not report it" — a turn whose
|
|
110
|
+
* `cachedInputTokens` is `null` contributes nothing rather than poisoning the
|
|
111
|
+
* total, so read this as a floor.
|
|
112
|
+
*/
|
|
113
|
+
readonly cachedInputTokens: number;
|
|
65
114
|
readonly costCents: number;
|
|
66
115
|
/** Sum of model `generationMs` across turns. */
|
|
67
116
|
readonly modelTimeMs: number;
|
|
@@ -75,6 +124,32 @@ export interface RunStats {
|
|
|
75
124
|
export declare function emptyRunStats(): RunStats;
|
|
76
125
|
/** Fold one completed model turn into cumulative run stats. */
|
|
77
126
|
export declare function accumulateTurn(stats: RunStats, turn: ModelTurnResult): RunStats;
|
|
127
|
+
/**
|
|
128
|
+
* Model spend that is not an agent turn — today, a compaction pass.
|
|
129
|
+
*
|
|
130
|
+
* Split out rather than folded through {@link accumulateTurn} because the two
|
|
131
|
+
* numbers answer different questions. A compaction is a real model call that
|
|
132
|
+
* costs real money and real latency, so its tokens, cost and time belong in the
|
|
133
|
+
* run's totals; it is *not* an iteration the agent spent making progress, so
|
|
134
|
+
* counting it in `turns` would make `stats.turns` incomparable with the loop's
|
|
135
|
+
* `maxIterations` and quietly overstate how much thinking the agent did.
|
|
136
|
+
*/
|
|
137
|
+
export interface AuxiliarySpend {
|
|
138
|
+
readonly inputTokens: number;
|
|
139
|
+
readonly outputTokens: number;
|
|
140
|
+
/**
|
|
141
|
+
* Required, unlike the two below, because a caller that cannot price a call
|
|
142
|
+
* still knows it cost *something* and should pass `0` deliberately rather
|
|
143
|
+
* than omit it. Time and cache figures are genuinely unknowable to some
|
|
144
|
+
* callers, so they are optional and contribute nothing when absent.
|
|
145
|
+
*/
|
|
146
|
+
readonly costCents: number;
|
|
147
|
+
/** Wall time of the auxiliary model call, if measured. */
|
|
148
|
+
readonly modelTimeMs?: number;
|
|
149
|
+
readonly cachedInputTokens?: number | null;
|
|
150
|
+
}
|
|
151
|
+
/** Fold auxiliary model spend into a run's totals without counting a turn. */
|
|
152
|
+
export declare function accumulateAuxiliarySpend(stats: RunStats, spend: AuxiliarySpend): RunStats;
|
|
78
153
|
/** Fold one dispatched tool call's duration into cumulative run stats. */
|
|
79
154
|
export declare function accumulateToolCall(stats: RunStats, toolName: string, durationMs: number): RunStats;
|
|
80
155
|
/**
|
package/contracts/turn.js
CHANGED
|
@@ -4,6 +4,7 @@ export function emptyRunStats() {
|
|
|
4
4
|
toolCalls: 0,
|
|
5
5
|
inputTokens: 0,
|
|
6
6
|
outputTokens: 0,
|
|
7
|
+
cachedInputTokens: 0,
|
|
7
8
|
costCents: 0,
|
|
8
9
|
modelTimeMs: 0,
|
|
9
10
|
toolTimeMs: 0,
|
|
@@ -20,11 +21,41 @@ export function accumulateTurn(stats, turn) {
|
|
|
20
21
|
turns: stats.turns + 1,
|
|
21
22
|
inputTokens: stats.inputTokens + turn.usage.inputTokens,
|
|
22
23
|
outputTokens,
|
|
24
|
+
cachedInputTokens: stats.cachedInputTokens + (turn.usage.cachedInputTokens ?? 0),
|
|
23
25
|
costCents: stats.costCents + (turn.usage.costCents ?? 0),
|
|
24
26
|
modelTimeMs,
|
|
25
27
|
outputTokensPerSecond: modelTimeMs > 0 ? (outputTokens / modelTimeMs) * 1000 : null,
|
|
26
28
|
};
|
|
27
29
|
}
|
|
30
|
+
/** Fold auxiliary model spend into a run's totals without counting a turn. */
|
|
31
|
+
export function accumulateAuxiliarySpend(stats, spend) {
|
|
32
|
+
const outputTokens = stats.outputTokens + spend.outputTokens;
|
|
33
|
+
const modelTimeMs = stats.modelTimeMs + (spend.modelTimeMs ?? 0);
|
|
34
|
+
return {
|
|
35
|
+
...stats,
|
|
36
|
+
inputTokens: stats.inputTokens + spend.inputTokens,
|
|
37
|
+
outputTokens,
|
|
38
|
+
cachedInputTokens: stats.cachedInputTokens + (spend.cachedInputTokens ?? 0),
|
|
39
|
+
costCents: stats.costCents + spend.costCents,
|
|
40
|
+
modelTimeMs,
|
|
41
|
+
// Recomputed, not carried: the added output tokens and model time both move
|
|
42
|
+
// the rate, and leaving the old value would report a throughput that
|
|
43
|
+
// matches neither the turns nor the totals now stored beside it.
|
|
44
|
+
outputTokensPerSecond: modelTimeMs > 0 ? (outputTokens / modelTimeMs) * 1000 : null,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Read a tool's accumulated time without going through `Object.prototype`.
|
|
49
|
+
*
|
|
50
|
+
* A bare `breakdown[name] ?? 0` reads inherited properties, so a tool legally
|
|
51
|
+
* named `constructor` or `toString` returns a *function*, and `fn + duration`
|
|
52
|
+
* silently produces a string — a corrupted `toolTimeBreakdownMs` entry that
|
|
53
|
+
* typechecks as `number`. Tool names come from the model, so this is reachable
|
|
54
|
+
* on any run. Same guard the plugin registry already applies to alias lookups.
|
|
55
|
+
*/
|
|
56
|
+
function ownDuration(breakdown, toolName) {
|
|
57
|
+
return Object.hasOwn(breakdown, toolName) ? breakdown[toolName] : 0;
|
|
58
|
+
}
|
|
28
59
|
/** Fold one dispatched tool call's duration into cumulative run stats. */
|
|
29
60
|
export function accumulateToolCall(stats, toolName, durationMs) {
|
|
30
61
|
return {
|
|
@@ -33,7 +64,7 @@ export function accumulateToolCall(stats, toolName, durationMs) {
|
|
|
33
64
|
toolTimeMs: stats.toolTimeMs + durationMs,
|
|
34
65
|
toolTimeBreakdownMs: {
|
|
35
66
|
...stats.toolTimeBreakdownMs,
|
|
36
|
-
[toolName]: (stats.toolTimeBreakdownMs
|
|
67
|
+
[toolName]: ownDuration(stats.toolTimeBreakdownMs, toolName) + durationMs,
|
|
37
68
|
},
|
|
38
69
|
};
|
|
39
70
|
}
|
|
@@ -67,14 +98,16 @@ export function accumulateRun(stats, run) {
|
|
|
67
98
|
...stats.toolTimeBreakdownMs,
|
|
68
99
|
};
|
|
69
100
|
for (const [toolName, durationMs] of Object.entries(run.toolTimeBreakdownMs)) {
|
|
101
|
+
// Own-property read, for the same reason as `accumulateToolCall`.
|
|
70
102
|
toolTimeBreakdownMs[toolName] =
|
|
71
|
-
(toolTimeBreakdownMs
|
|
103
|
+
ownDuration(toolTimeBreakdownMs, toolName) + durationMs;
|
|
72
104
|
}
|
|
73
105
|
return {
|
|
74
106
|
turns: stats.turns + run.turns,
|
|
75
107
|
toolCalls: stats.toolCalls + run.toolCalls,
|
|
76
108
|
inputTokens: stats.inputTokens + run.inputTokens,
|
|
77
109
|
outputTokens,
|
|
110
|
+
cachedInputTokens: stats.cachedInputTokens + run.cachedInputTokens,
|
|
78
111
|
costCents: stats.costCents + run.costCents,
|
|
79
112
|
modelTimeMs,
|
|
80
113
|
toolTimeMs: stats.toolTimeMs + run.toolTimeMs,
|
package/index.d.ts
CHANGED
|
@@ -11,9 +11,12 @@
|
|
|
11
11
|
* coalesced heartbeat, failure classification, tool-batch pooling, child-run
|
|
12
12
|
* lineage and admission), the streaming-completion watchdog and completion
|
|
13
13
|
* defect detection (`src/completion/`), transcript validation/healing, provider
|
|
14
|
-
* tool-schema sanitization,
|
|
14
|
+
* tool-schema sanitization, the plugin/tool vocabulary with its registry and
|
|
15
15
|
* progressive-disclosure activation — generic over the host's invocation
|
|
16
|
-
* context
|
|
16
|
+
* context — and the skill vocabulary that applies the same disclosure to
|
|
17
|
+
* instructions (`src/skills/` — the code-skill registry, the `SKILL.md` codec,
|
|
18
|
+
* the content hash that makes a replay honest, the catalog budget, the
|
|
19
|
+
* active-instruction resolver and its activation controller). What is NOT here is
|
|
17
20
|
* the run driver: starting a run, recording what it did, and delivering its
|
|
18
21
|
* output. See the README for the rest of what is deliberately absent.
|
|
19
22
|
*/
|
|
@@ -24,4 +27,5 @@ export * from "./run/index.js";
|
|
|
24
27
|
export * from "./transcript/index.js";
|
|
25
28
|
export * from "./tools/index.js";
|
|
26
29
|
export * from "./plugins/index.js";
|
|
30
|
+
export * from "./skills/index.js";
|
|
27
31
|
export * from "./loop/index.js";
|