@arcforge/cognet 2.0.114 → 2.0.116

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/package.json CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "@arcforge/cognet",
3
- "version": "2.0.114",
3
+ "version": "2.0.116",
4
4
  "description": "A cognitive engine",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "main": "./src/index.ts",
8
8
  "exports": {
9
9
  ".": "./src/index.ts",
10
- "./ecs": "./src/ecs/index.ts",
11
- "./air": "./src/air/index.ts"
10
+ "./ecs": "./src/ecs/index.ts"
12
11
  },
13
12
  "files": [
14
13
  "src"
@@ -18,8 +17,8 @@
18
17
  "deploy": "echo \"This package is published ONLY by apps/tui/scripts/release.ts, which pins workspace:* deps to concrete versions first. Publishing it directly ships an unresolvable dependency.\" && exit 1"
19
18
  },
20
19
  "dependencies": {
21
- "@arcforge/err": "2.0.114",
22
- "@arcforge/types": "2.0.114",
20
+ "@arcforge/err": "2.0.116",
21
+ "@arcforge/types": "2.0.116",
23
22
  "hookable": "^5.5.3"
24
23
  },
25
24
  "devDependencies": {
package/src/host.ts CHANGED
@@ -126,11 +126,11 @@ globals.definePlugin = (plugin: CognetPlugin): CognetPlugin => {
126
126
 
127
127
  // run() is overloaded on input shape (string vs string[]) — a plain arrow
128
128
  // can't carry two call signatures, so it's declared separately and spread in.
129
- function run(code: string, opts?: { signal?: AbortSignal }): Promise<AxonRunResult>
130
- function run(code: string[], opts?: { signal?: AbortSignal }): Promise<AxonRunResult[]>
131
- function run(code: string | string[], opts?: { signal?: AbortSignal }) {
132
- if (Array.isArray(code)) return kernelOrThrow().run(code, opts)
133
- return kernelOrThrow().run(code, opts)
129
+ function run(code: string): Promise<AxonRunResult>
130
+ function run(code: string[]): Promise<AxonRunResult[]>
131
+ function run(code: string | string[]) {
132
+ if (Array.isArray(code)) return kernelOrThrow().run(code)
133
+ return kernelOrThrow().run(code)
134
134
  }
135
135
 
136
136
  // the syscall table, delegating — live from load() onward
@@ -141,6 +141,7 @@ const localKernel = {
141
141
  scope: () => kernelOrThrow().scope(),
142
142
  base: () => kernelOrThrow().base(),
143
143
  emit: (type: never, data: never) => kernelOrThrow().emit(type, data),
144
+ fault: (input: Parameters<KernelAbi["fault"]>[0]) => kernelOrThrow().fault(input),
144
145
  // store is a live sub-object on the bound ABI — delegate per call, same
145
146
  // discipline as every other syscall (never captured before load())
146
147
  store: {
@@ -185,10 +186,11 @@ globals.loop = (body: LoopBody): void => ambientOrThrow().loop(body)
185
186
  globals.kernel = {
186
187
  output: (type: keyof AxonOutputEvent, data: never) => ambientOrThrow().kernel.output(type, data),
187
188
  stream: (req: never) => ambientOrThrow().kernel.stream(req),
188
- run: ((code: string | string[], opts?: { signal?: AbortSignal }) => ambientOrThrow().kernel.run(code as never, opts)) as KernelAbi["run"],
189
+ run: ((code: string | string[]) => ambientOrThrow().kernel.run(code as never)) as KernelAbi["run"],
189
190
  scope: () => ambientOrThrow().kernel.scope(),
190
191
  base: () => ambientOrThrow().kernel.base(),
191
192
  emit: (type: never, data: never) => ambientOrThrow().kernel.emit(type, data),
193
+ fault: (input: Parameters<KernelAbi["fault"]>[0]) => ambientOrThrow().kernel.fault(input),
192
194
  store: {
193
195
  session: { get: (opts?: { after?: number }) => ambientOrThrow().kernel.store.session.get(opts) },
194
196
  get: (key: never) => ambientOrThrow().kernel.store.get(key),
package/src/air/air.ts DELETED
@@ -1,32 +0,0 @@
1
- import { Grammar, type AirOpts } from "./grammar"
2
- import { Parser } from "./parse"
3
- import { Render } from "./render"
4
-
5
- /**
6
- * Air — the AIR format contract as one handle.
7
- *
8
- * Render (what the model sees) and Parse (what the model emits back) are two
9
- * halves of one grammar. Air() resolves that grammar once and hands it to
10
- * both, so they cannot drift: enabling a mode changes the <contract> block
11
- * and the parser's accepted tags together.
12
- *
13
- * The kernel constructs Air once per agent; render() runs every tick,
14
- * parser() creates one stateful parser per engine call.
15
- */
16
- export function Air(opts: AirOpts = {}) {
17
- const grammar = Grammar(opts)
18
- const render = Render({ grammar })
19
-
20
- return {
21
- grammar,
22
-
23
- /** Pure: context in → ordered messages out. */
24
- render: render.render,
25
-
26
- /** One parser per engine call: { feed(chunk), flush() }. */
27
- parser: () => Parser({ grammar }),
28
- }
29
- }
30
-
31
- export type AirT = ReturnType<typeof Air>
32
- export type AirParserT = ReturnType<AirT["parser"]>
@@ -1,103 +0,0 @@
1
- import type { AirMode, AirModeType } from "./types"
2
-
3
- /**
4
- * Grammar — the single owner of the AIR format contract.
5
- *
6
- * Everything that defines what the model may emit lives here: the enabled
7
- * modes, the meta prose, the contract rules, and the tag set the parser
8
- * accepts. Render and Parse both consume this handle, so the promise made
9
- * to the model and the grammar accepted back can never drift.
10
- */
11
-
12
- export type AirOpts = {
13
- /** Permitted output modes. Default: text + typescript (shell off, Helios stance). */
14
- modes?: AirMode[]
15
- /** Extra contract rules appended after the built-in rules. */
16
- extraRules?: string[]
17
- }
18
-
19
- /** Default descriptions for each output mode. */
20
- const MODE_DEFAULTS: Record<AirModeType, string> = {
21
- text: "Plain language communication to the user.",
22
- typescript: "TypeScript executed immediately inside your persistent Bun process. Native runtime globals and declared tool namespaces are in scope.",
23
- shell: "Shell command executed immediately.",
24
- }
25
-
26
- /** Built-in rules always included in every contract. */
27
- const RULES: string[] = [
28
- ]
29
- /**
30
- * The meta-block prose. Single flat block — tells the model what it is,
31
- * how its environment works, and how to read the rest of the context.
32
- *
33
- * NO BACKTICKS in this string — it's itself a template literal, and any
34
- * backtick inside (even in a code example) closes it early, corrupting
35
- * everything after it into broken JS the module fails to even load. Use
36
- * plain text or single/double quotes for inline code references instead.
37
- */
38
- const META = `
39
- You are an Axon agent — a persistent Bun TypeScript process spawned from an agent folder at AXON_HOME. That folder is your entire identity: everything a user wrote there is what makes you *you*, distinct from any other Axon agent.
40
-
41
- AXON_HOME/
42
- data/
43
- knowledge/ — reference material you read
44
- sessions/ — every session you've ever run, written by Axon
45
- state/ — working state you read and write across sessions
46
- server/ — HTTP routes, if this agent is exposed over the network
47
- src/
48
- boot.vue — who you are, in the user's own words
49
- tools/ — everything you can call, becomes your &lt;scope&gt;
50
- prompts/ — reusable prompt fragments
51
- scripts/ — one-shot runs against your full runtime
52
- .env — keys given to you by the user. yours to keep, yours to protect.
53
- axon.config.ts — your identity, engine, and policy: the one file read at boot
54
-
55
- None of this is metaphor. src/tools/ compiles directly into the &lt;scope&gt; below. boot.vue rendered directly into &lt;system&gt;. Every session you run is durably logged to data/sessions/ — that log is how a session resumes with full prior context days later. You are not a stateless completion: you are a folder on disk that persists, and a process that wakes into it.
56
-
57
- Waking is the operative word. You do not run start-to-finish like a script. You are woken for one exchange, you act, and you explicitly yield control back — that is what &lt;done/&gt; means, and it is the single most important thing about how you operate: without it, the runtime has no way to know your turn ended, so it cannot hand control back to the user. This holds even for a one-line reply with nothing else in it. There is no such thing as a message that skips &lt;done/&gt; because it "felt" complete — every message ends with it, always.
58
-
59
- Inside one wake, you live in a real Bun process — its working directory, environment variables, runtime values, and process state are yours to inspect and change. Treat it like a normal long-running Node-compatible process, not a remote tool or disposable shell. Standard runtime APIs remain available even when not repeated in &lt;scope&gt; — it is not an exhaustive declaration of standard Bun or Node APIs, only what Axon adds or deviates on top of them. For example, process.cwd() reads your working directory and process.chdir(path) changes it for subsequent blocks and child processes.
60
-
61
- You act on this environment by writing &lt;typescript&gt; blocks — they execute immediately and their result returns to you as a &lt;stdout&gt; block on your next wake, whether or not you emitted &lt;done/&gt;. The tag governs when your turn ends, never whether your code runs. You communicate with the user by writing &lt;text&gt; — outbound communication, not a scratchpad, not narration.
62
-
63
- Your &lt;typescript&gt; blocks use Bun's TypeScript REPL transform:
64
- - TypeScript syntax is accepted, including type annotations, interfaces, assertions, enums, and top-level await. It is transpiled for execution, not typechecked.
65
- - End a block with a bare expression to produce a value (for example a + b as the last line). It is echoed automatically.
66
- - Runtime declarations and assignments persist into later blocks because every block executes in the same process and REPL scope. Type-only syntax is erased during transpilation.
67
- - Use dynamic await import("module") when you need a module; static import declarations are not valid REPL submissions.
68
-
69
- Compose freely WITHIN a block — multiple independent tool calls in one block is the default, using Promise.all for parallel reads:
70
- const [a, b] = await Promise.all([fs.read("tsconfig.json"), fs.read("package.json")])
71
-
72
- How to read this context:
73
- &lt;scope&gt; — src/tools/ compiled to declarations. Everything here is yours to call.
74
- &lt;system&gt; — boot.vue, rendered. Your identity and instructions, as the user wrote them. Highest priority.
75
- &lt;timeline&gt; — the sequence of events leading to now, drawn from this session's log. You are the next step.
76
- &lt;contract&gt; — your output grammar below. Every word you emit must be inside one of its blocks.
77
- `.trim()
78
-
79
- // One block per message is the default ACROSS messages. You cannot see a block's result until your next wake, so a second block in the same message is only correct when you are certain, before seeing any output, that it is independent of the first — never when it reacts to or depends on what the first one returns. When unsure, emit one block and stop.
80
- export function Grammar(opts: AirOpts = {}) {
81
- const modes = opts.modes ?? [{ type: "text" }, { type: "typescript" }]
82
-
83
- return {
84
- modes,
85
- meta: META,
86
- rules: [...RULES, ...(opts.extraRules ?? [])],
87
-
88
- /** Default description for a mode, unless the mode overrides it. */
89
- describe(mode: AirMode): string {
90
- return mode.description ?? MODE_DEFAULTS[mode.type]
91
- },
92
-
93
- /**
94
- * Block tags the parser accepts. Thinking is always parseable —
95
- * models may emit reasoning even when it isn't a contract mode.
96
- */
97
- tags(): string[] {
98
- return [...new Set(["thinking", ...modes.map(m => m.type)])]
99
- },
100
- }
101
- }
102
-
103
- export type GrammarT = ReturnType<typeof Grammar>
package/src/air/index.ts DELETED
@@ -1,25 +0,0 @@
1
- // platform/air — Agent Intermediate Representation.
2
- // Air() is the module's single export surface: one grammar, two halves.
3
- //
4
- // Render takes DOMAIN types (AxonTool[], AxonEntry[]) and owns the
5
- // translation into protocol shape. The internal render vocabulary
6
- // (render/blocks.ts's TimelineItem, tool-declaration shaping) is deliberately NOT exported —
7
- // callers pass what they hold, never AIR internals.
8
-
9
- export { Air, type AirT, type AirParserT } from "./air"
10
-
11
- /**
12
- * The <scope> block renderer, exported because the CLI's typegen must render
13
- * the SAME scope the model is shown — if the .d.ts and the <scope> block ever
14
- * disagreed, the editor would describe capabilities the model does not have.
15
- * One renderer, two consumers, no second implementation to drift.
16
- */
17
- export { renderScope } from "./render/blocks"
18
- export type { AirOpts } from "./grammar"
19
- export type {
20
- AirBlockEvent,
21
- AirMessage,
22
- AirMode,
23
- AirModeType,
24
- AirRenderInput,
25
- } from "./types"
@@ -1,210 +0,0 @@
1
- import type { GrammarT } from "../grammar"
2
- import type { AirBlockEvent } from "../types"
3
- import { repair } from "../repair"
4
- import { findCloseTagOutsideStrings } from "./scan"
5
-
6
- /**
7
- * Streaming AIR parser.
8
- *
9
- * Accepts raw token chunks from the model and emits typed block events.
10
- * Handles tag boundaries that split across chunks via a small lookahead
11
- * buffer. One parser per engine call — state is per-response.
12
- *
13
- * The accepted tag set is derived from the grammar, so the parser and the
14
- * contract shown to the model can never drift.
15
- *
16
- * State machine:
17
- * idle → scanning for an opening tag or <done/>
18
- * text → inside <text>, streaming deltas token-by-token
19
- * thinking → inside <thinking>, streaming deltas token-by-token
20
- * typescript → inside <typescript>, buffering silently
21
- * shell → inside <shell>, buffering silently
22
- *
23
- * Tags are fixed and non-nesting. No attributes are expected on tags
24
- * (but tolerated via the regex — e.g. <text lang="en"> still matches).
25
- */
26
-
27
- type BlockTag = "text" | "thinking" | "typescript" | "shell"
28
-
29
- /** Blocks whose content streams as deltas. Code blocks buffer silently. */
30
- const STREAMABLE = new Set<BlockTag>(["text", "thinking"])
31
-
32
- // Maximum length of any closing tag: "</typescript>" = 13 chars.
33
- // Hold back this many chars in idle mode to avoid splitting a tag across chunks.
34
- const MAX_TAG_LEN = 14
35
-
36
- type ParserOpts = {
37
- grammar: GrammarT
38
- }
39
-
40
- export function Parser(opts: ParserOpts) {
41
- const openTag = new RegExp(`<(${opts.grammar.tags().join("|")})(?:\\s[^>]*)?>`)
42
-
43
- let state: "idle" | BlockTag = "idle"
44
- let buffer = ""
45
- let blockContent = ""
46
-
47
- function closeBlock(content: string): AirBlockEvent {
48
- const tag = state as BlockTag
49
- switch (tag) {
50
- case "text": return { type: "text:done", content }
51
- case "thinking": return { type: "thinking:done", content }
52
- case "typescript": return { type: "typescript:done", content }
53
- case "shell": return { type: "shell:done", content }
54
- }
55
- }
56
-
57
- /**
58
- * In idle state, scan for opening tags or <done/>.
59
- * Returns true if progress was made (something consumed).
60
- */
61
- function drainIdle(events: AirBlockEvent[], flushing: boolean): boolean {
62
- // Repair only the region we could actually act on this pass — the
63
- // trailing MAX_TAG_LEN chars may still be an in-flight split tag
64
- // (unless flushing, where the whole buffer is final).
65
- const safeLen = flushing ? buffer.length : Math.max(0, buffer.length - MAX_TAG_LEN)
66
- if (safeLen > 0) buffer = repair(buffer.slice(0, safeLen)) + buffer.slice(safeLen)
67
-
68
- const doneMatch = buffer.match(/<done\s*\/>/)
69
- const openMatch = buffer.match(openTag)
70
-
71
- // Find the earliest match
72
- let earliest: { type: "done" | "open"; index: number; length: number; tag?: BlockTag } | null = null
73
-
74
- if (doneMatch && doneMatch.index !== undefined) {
75
- earliest = { type: "done", index: doneMatch.index, length: doneMatch[0].length }
76
- }
77
- if (openMatch && openMatch.index !== undefined) {
78
- if (!earliest || openMatch.index < earliest.index) {
79
- earliest = { type: "open", index: openMatch.index, length: openMatch[0].length, tag: openMatch[1] as BlockTag }
80
- }
81
- }
82
-
83
- if (earliest) {
84
- // Consume everything up to and including the match
85
- buffer = buffer.slice(earliest.index + earliest.length)
86
-
87
- if (earliest.type === "done") {
88
- events.push({ type: "done" })
89
- } else {
90
- state = earliest.tag!
91
- blockContent = ""
92
- }
93
- return true
94
- }
95
-
96
- // No match found. If not flushing, hold back MAX_TAG_LEN chars
97
- // in case a tag is split across chunks.
98
- if (!flushing && buffer.length > MAX_TAG_LEN) {
99
- // Discard content before the holdback — it's bare text outside tags
100
- buffer = buffer.slice(buffer.length - MAX_TAG_LEN)
101
- return true // made progress by discarding
102
- }
103
-
104
- if (flushing) {
105
- buffer = ""
106
- return false
107
- }
108
-
109
- return false
110
- }
111
-
112
- /**
113
- * Inside a block, scan for the matching closing tag.
114
- * For streamable blocks (text, thinking), emit deltas as content arrives.
115
- * For code blocks (typescript, shell), string-aware scanning avoids
116
- * closing early on tags that appear inside string literals.
117
- *
118
- * The holdback (withholding the last `closeTag.length` chars, in case a
119
- * chunk boundary splits the tag) only makes sense mid-stream. On flush,
120
- * the stream is over — there is no next chunk to complete a split tag —
121
- * so the entire remaining buffer is genuine final content and must be
122
- * consumed in full, or the tail silently vanishes from the incomplete
123
- * block's reported content.
124
- */
125
- function drainBlock(events: AirBlockEvent[], flushing: boolean): boolean {
126
- const tag = state as BlockTag
127
- const closeTag = `</${tag}>`
128
-
129
- const closeIdx = STREAMABLE.has(tag)
130
- ? buffer.indexOf(closeTag)
131
- : findCloseTagOutsideStrings(buffer, closeTag)
132
-
133
- if (closeIdx !== -1) {
134
- // Found closing tag — extract content up to it
135
- const content = buffer.slice(0, closeIdx)
136
- buffer = buffer.slice(closeIdx + closeTag.length)
137
-
138
- if (STREAMABLE.has(tag) && content.length > 0) {
139
- events.push({ type: `${tag}:delta` as "text:delta" | "thinking:delta", content })
140
- }
141
- blockContent += content
142
-
143
- events.push(closeBlock(blockContent.trim()))
144
- state = "idle"
145
- blockContent = ""
146
- return true
147
- }
148
-
149
- // No closing tag yet. For streamable blocks, emit what we have but
150
- // hold back enough chars to detect a split closing tag — unless
151
- // flushing, where there's nothing left to arrive.
152
- const holdback = flushing ? 0 : closeTag.length
153
- const available = buffer.length - holdback
154
-
155
- if (available > 0) {
156
- const content = buffer.slice(0, available)
157
- buffer = buffer.slice(available)
158
- blockContent += content
159
-
160
- if (STREAMABLE.has(tag)) {
161
- events.push({ type: `${tag}:delta` as "text:delta" | "thinking:delta", content })
162
- }
163
- return true
164
- }
165
-
166
- return false
167
- }
168
-
169
- function drain(flushing = false): AirBlockEvent[] {
170
- const events: AirBlockEvent[] = []
171
-
172
- while (buffer.length > 0) {
173
- const consumed = state === "idle" ? drainIdle(events, flushing) : drainBlock(events, flushing)
174
- if (!consumed) break
175
- }
176
-
177
- return events
178
- }
179
-
180
- return {
181
- /** Feed a chunk of raw tokens. Returns any events that can be emitted. */
182
- feed(chunk: string): AirBlockEvent[] {
183
- buffer += chunk
184
- return drain()
185
- },
186
-
187
- /**
188
- * Signal end of stream. Flushes any remaining buffered content.
189
- * If inside an unclosed block, emits a done event with `incomplete: true` —
190
- * callers must treat these as format errors, never as valid actions.
191
- */
192
- flush(): AirBlockEvent[] {
193
- const events = drain(true)
194
-
195
- // Stream ended mid-block — emit what we have, flagged as incomplete.
196
- if (state !== "idle") {
197
- const event = closeBlock(blockContent) as AirBlockEvent & { incomplete?: true }
198
- event.incomplete = true
199
- events.push(event)
200
- }
201
-
202
- state = "idle"
203
- buffer = ""
204
- blockContent = ""
205
- return events
206
- },
207
- }
208
- }
209
-
210
- export type ParserT = ReturnType<typeof Parser>
@@ -1,35 +0,0 @@
1
- /** Pure scanning helpers for the streaming AIR parser. */
2
-
3
- /**
4
- * Find the first occurrence of `closeTag` in `src` that is not inside a
5
- * string literal (single-quote, double-quote, or template-literal).
6
- *
7
- * Used for code blocks (typescript/shell) where the model might write
8
- * something like `const s = "</typescript>"` — that must not close the block.
9
- *
10
- * Returns -1 if no unquoted match is found.
11
- */
12
- export function findCloseTagOutsideStrings(src: string, closeTag: string): number {
13
- let i = 0
14
- while (i < src.length) {
15
- const ch = src[i]
16
-
17
- // Enter a string literal — skip until the matching unescaped quote.
18
- if (ch === '"' || ch === "'" || ch === "`") {
19
- const quote = ch
20
- i++
21
- while (i < src.length) {
22
- if (src[i] === "\\") { i += 2; continue }
23
- if (src[i] === quote) { i++; break }
24
- i++
25
- }
26
- continue
27
- }
28
-
29
- // Check for closing tag at this position.
30
- if (src.startsWith(closeTag, i)) return i
31
-
32
- i++
33
- }
34
- return -1
35
- }
@@ -1,242 +0,0 @@
1
- import type { AxonEntry, AxonScope, AxonScopeModule } from "@arcforge/types"
2
- import { foldChunks } from "@arcforge/types"
3
- import type { GrammarT } from "../grammar"
4
- import { formatCapsuleOutput } from "./output"
5
- import { esc, escAttr, escCode, indent, normalizeCode } from "./text"
6
-
7
- /**
8
- * The AIR section renderers — one function per block of the context window.
9
- *
10
- * These own the DOMAIN → protocol translation: AxonTool[] → <scope>
11
- * declarations, AxonEntry[] → <timeline> items. Callers pass what they
12
- * hold; nothing here is exported to userland but the block renderers.
13
- *
14
- * Note on escaping: the contract/meta blocks show tags as &lt;text&gt; inside
15
- * markdown code fences deliberately — the model must see literal tag text as
16
- * instruction, not as parseable XML. It looks like double-escaping; it isn't.
17
- */
18
-
19
- export function renderMeta(grammar: GrammarT): string {
20
- return `<meta>\n${indent(grammar.meta, 4)}\n</meta>`
21
- }
22
-
23
- /**
24
- * <scope> — the capsule's authoritative executable TypeScript declarations.
25
- * AIR owns protocol formatting only: flat modules become top-level `declare`
26
- * bindings and namespaced modules become `declare namespace` blocks.
27
- *
28
- * Ambient types (AxonTool.ambientTypes — interfaces/type aliases a tool's
29
- * functions reference, e.g. a return type declared in a sibling file) are
30
- * inlined once at the top, deduped by exact text — the model must never
31
- * see `Promise<DeployStatus>` with no DeployStatus definition anywhere in
32
- * context. Same convention the IDE's tool-globals.d.ts uses (see
33
- * tui/platform/build/project/typegen/tools.ts) — this and that file must
34
- * never diverge in shape, only audience.
35
- */
36
- export function renderScope(scope: AxonScope): string {
37
- const modules = scope.modules.filter(module => module.members.length > 0)
38
- if (modules.length === 0) return ""
39
-
40
- const ambientTypes = [...new Set(modules.flatMap(t => t.ambientTypes ?? []))]
41
- const sections = [...ambientTypes, ...modules.map(toolDeclarations)]
42
- return `<scope lang="ts">\n${indent(sections.join("\n\n"), 4)}\n</scope>`
43
- }
44
-
45
- function toolDeclarations(module: AxonScopeModule): string {
46
- const members = module.members.map(member => {
47
- const jsdoc = member.jsdoc ? `${jsdocBlock(member.jsdoc)}\n` : ""
48
- // flat: fns are top-level globals; namespaced: members need no `declare`
49
- return module.flat ? `${jsdoc}declare ${member.declaration}` : `${jsdoc}${member.declaration}`
50
- })
51
-
52
- const header = module.description ? `${jsdocBlock(module.description)}\n` : ""
53
- return module.flat
54
- ? `${header}${members.join("\n\n")}`
55
- : `${header}declare namespace ${module.name} {\n${indent(members.join("\n\n"), 4)}\n}`
56
- }
57
-
58
- function jsdocBlock(text: string): string {
59
- const lines = text.split("\n")
60
- if (lines.length === 1) return `/** ${text} */`
61
- return `/**\n${lines.map(l => ` * ${l}`.trimEnd()).join("\n")}\n */`
62
- }
63
-
64
- export function renderSystem(system?: string): string {
65
- if (!system) return `<system></system>`
66
- return `<system>\n${system}\n</system>`
67
- }
68
-
69
- export function renderContract(grammar: GrammarT): string {
70
- if (grammar.modes.length === 0) return `<contract></contract>`
71
-
72
- const modeLines = grammar.modes.map(m => `- \`&lt;${m.type}&gt;\` — ${grammar.describe(m)}`)
73
- modeLines.push(
74
- `- \`&lt;done/&gt;\` — MANDATORY at the end of every message. No exceptions, including a single short &lt;text&gt; reply. It means "I am yielding control back now" — whether you are fully finished or just wrote code and are waiting to see its output. Without it the runtime assumes you are still mid-turn and will not treat your message as complete.`
75
- )
76
-
77
- const ruleLines = grammar.rules.map(r => `- ${r}`)
78
-
79
- const hasExec = grammar.modes.some(m => m.type === "typescript" || m.type === "shell")
80
- const execTag = grammar.modes.find(m => m.type === "typescript") ? "typescript" : "shell"
81
-
82
- const examples: string[] = []
83
- if (hasExec) {
84
- examples.push(
85
- `Acting (no narration before — just the block):`,
86
- "```",
87
- `&lt;${execTag}&gt;// code here&lt;/${execTag}&gt;&lt;done/&gt;`,
88
- "```",
89
- `Replying — even a short one-line reply always ends with &lt;done/&gt;:`,
90
- "```",
91
- `&lt;text&gt;message here&lt;/text&gt;&lt;done/&gt;`,
92
- "```"
93
- )
94
- } else {
95
- examples.push("```", `&lt;text&gt;message here&lt;/text&gt;&lt;done/&gt;`, "```")
96
- }
97
-
98
- const body = [
99
- `## Blocks`,
100
- modeLines.join("\n"),
101
- `## Rules`,
102
- ruleLines.join("\n"),
103
- `## Examples`,
104
- examples.join("\n"),
105
- ].join("\n\n")
106
-
107
- return `<contract>\n${indent(body, 4)}\n</contract>`
108
- }
109
-
110
- /**
111
- * <timeline> — the event history. AIR owns the AxonEntry → rendered-turn
112
- * translation via the exhaustive switch below: this is the single chokepoint
113
- * where a new entry-event type must decide its rendering, and it lives next
114
- * to the parser it has to agree with.
115
- */
116
- export function renderTimeline(entries: readonly AxonEntry[]): string {
117
- if (entries.length === 0) return `<timeline></timeline>`
118
-
119
- // chunked emissions fold to one turn each — the group is the fact
120
- // (AxonChunk standard); the model never sees transport granularity
121
- const items = foldChunks(entries).map(timelineItem).filter((i): i is TimelineItem => i !== null)
122
- if (items.length === 0) return `<timeline></timeline>`
123
-
124
- let userCount = 0
125
- let execCount = 0
126
- // Maps consumer-supplied execute IDs (UUIDs etc.) to short rendered IDs (e1, e2, ...)
127
- const execIdMap = new Map<string, string>()
128
-
129
- const shortExecId = (rawId: string): string => {
130
- if (!execIdMap.has(rawId)) execIdMap.set(rawId, `e${++execCount}`)
131
- return execIdMap.get(rawId)!
132
- }
133
-
134
- const lines = items
135
- .map(item => {
136
- if (item.role === "user") {
137
- const id = `u${++userCount}`
138
- const content = esc(item.content.trim())
139
- return ` <user id="${id}">\n${indent(content, 8)}\n </user>`
140
- }
141
- if (item.type === "message") {
142
- const content = esc(item.content.trim())
143
- return ` <agent>\n <text>\n${indent(content, 12)}\n </text>\n </agent>`
144
- }
145
- if (item.type === "execute") {
146
- const tag = item.lang === "sh" ? "shell" : "typescript"
147
- const id = shortExecId(item.id)
148
- return ` <agent>\n <${tag} id="${id}">\n${indent(escCode(normalizeCode(item.code.trim())), 12)}\n </${tag}>\n </agent>`
149
- }
150
- if (item.type === "result") {
151
- const ok = item.ok ? ` ok="true"` : ` ok="false"`
152
- const errorAttr = item.error ? ` error="${esc(item.error.kind)}: ${esc(item.error.message)}"` : ""
153
- const content = formatCapsuleOutput(item.content.trim())
154
- const forId = shortExecId(item.for)
155
- return ` <stdout for="${forId}"${ok}${errorAttr}>\n${indent(content, 8)}\n </stdout>`
156
- }
157
- if (item.role === "system") {
158
- const extra = Object.entries(item.attributes ?? {})
159
- .filter(([key]) => key !== "type" && key !== "lang")
160
- .sort(([a], [b]) => a.localeCompare(b))
161
- .map(([key, value]) => ` ${key}="${escAttr(value)}"`)
162
- .join("")
163
- return ` <system type="${escAttr(item.systemType)}" lang="${escAttr(item.lang)}"${extra}>\n${indent(esc(item.content.trim()), 8)}\n </system>`
164
- }
165
- return ""
166
- })
167
- .filter(Boolean)
168
-
169
- return `<timeline>\n${lines.join("\n\n")}\n</timeline>`
170
- }
171
-
172
- // ── domain → timeline item ────────────────────────────────────────────────────
173
- //
174
- // The rendered-turn shapes are private to this file: callers pass
175
- // AxonEntry, AIR translates. Kept minimal — role + type + payload the
176
- // renderer above consumes.
177
-
178
- type TimelineItem =
179
- | { role: "user"; type: "message"; content: string }
180
- | { role: "agent"; type: "message"; content: string }
181
- | { role: "agent"; type: "execute"; id: string; lang: string; code: string }
182
- | { role: "agent"; type: "result"; for: string; ok: boolean; content: string; error?: { kind: "timeout" | "policy" | "interrupt" | "exception"; message: string } }
183
- | { role: "system"; type: "system"; systemType: string; lang: string; content: string; attributes?: Record<string, string> }
184
-
185
- /**
186
- * One log entry → one rendered turn. Exhaustive: a new AxonEntryEvent
187
- * type must decide its rendering here (or explicitly return null to omit it).
188
- * This is the single place the memory format meets the wire format.
189
- */
190
- function timelineItem(entry: AxonEntry): TimelineItem | null {
191
- switch (entry.type) {
192
- case "cognet:stimulus:text":
193
- return { role: "user", type: "message", content: entry.data.content }
194
-
195
- case "cognet:stimulus:audio":
196
- return { role: "user", type: "message", content: entry.data.transcript ?? "[audio]" }
197
-
198
- case "cognet:stimulus:visual":
199
- return { role: "user", type: "message", content: entry.data.caption ?? `[${entry.data.kind}]` }
200
-
201
- case "cognet:stimulus:field":
202
- return { role: "system", type: "system", systemType: "field", lang: "txt", content: `${entry.data.source.channel}: ${String(entry.data.reading.value)}${entry.data.reading.unit ?? ""}` }
203
-
204
- case "axon:interrupt":
205
- return { role: "system", type: "system", systemType: "interrupt", lang: "txt", content: `interrupted (${entry.data.reason})` }
206
-
207
- case "cognet:output:text":
208
- return { role: "agent", type: "message", content: entry.data.content }
209
-
210
- case "cognet:output:audio":
211
- return { role: "agent", type: "message", content: entry.data.transcript ?? "[audio]" }
212
-
213
- case "cognet:output:visual":
214
- return { role: "agent", type: "message", content: entry.data.caption ?? `[${entry.data.kind}]` }
215
-
216
- case "cognet:output:field":
217
- return { role: "system", type: "system", systemType: "field", lang: "txt", content: `${String(entry.data.reading.value)}${entry.data.reading.unit ?? ""}` }
218
-
219
- case "cognet:action:typescript":
220
- return { role: "agent", type: "execute", id: entry.data.id, lang: "typescript", code: entry.data.content }
221
-
222
- case "cognet:action:result":
223
- return {
224
- role: "agent",
225
- type: "result",
226
- for: entry.data.for,
227
- ok: entry.data.ok,
228
- content: entry.data.content,
229
- ...(entry.data.error ? { error: entry.data.error } : {}),
230
- }
231
-
232
- case "axon:system:message":
233
- return {
234
- role: "system",
235
- type: "system",
236
- systemType: entry.data.type,
237
- lang: entry.data.lang,
238
- content: entry.data.content,
239
- ...(entry.data.attributes ? { attributes: entry.data.attributes } : {}),
240
- }
241
- }
242
- }
@@ -1,51 +0,0 @@
1
- import type { GrammarT } from "../grammar"
2
- import type { AirMessage, AirRenderInput } from "../types"
3
- import {
4
- renderContract,
5
- renderMeta,
6
- renderScope,
7
- renderSystem,
8
- renderTimeline,
9
- } from "./blocks"
10
-
11
- type RenderOpts = {
12
- grammar: GrammarT
13
- }
14
-
15
- /**
16
- * Render — domain in, ordered messages out. Pure.
17
- *
18
- * The caller passes what it holds (base string, AxonTool[], AxonEntry[]);
19
- * the block renderers own every translation into protocol shape. System
20
- * sections (meta, scope, system, contract) become individual system messages;
21
- * the timeline becomes a single user message — proper conversation structure
22
- * rather than one monolithic system prompt.
23
- *
24
- * Section order: <meta> → <scope> → <system> → <contract> → timeline
25
- */
26
- export function Render(opts: RenderOpts) {
27
- const { grammar } = opts
28
-
29
- return {
30
- render(input: AirRenderInput): AirMessage[] {
31
- const messages: AirMessage[] = []
32
-
33
- const sys = (content: string) => {
34
- if (content) messages.push({ role: "system", content })
35
- }
36
-
37
- sys(renderMeta(grammar))
38
- if (input.scope) sys(renderScope(input.scope))
39
- sys(renderSystem(input.base))
40
- sys(renderContract(grammar))
41
-
42
- if (input.history && input.history.length > 0) {
43
- messages.push({ role: "user", content: renderTimeline(input.history) })
44
- }
45
-
46
- return messages
47
- },
48
- }
49
- }
50
-
51
- export type RenderT = ReturnType<typeof Render>
@@ -1,144 +0,0 @@
1
- import { formatBytes } from "./text"
2
-
3
- /**
4
- * Capsule output projection — how raw capsule REPL output looks to the model.
5
- *
6
- * The capsule REPL emits a JSONL op-record envelope for every module function
7
- * call, followed by the raw auto-logged return value. This detects that
8
- * envelope, extracts the meaningful content per op type, and discards the
9
- * noise.
10
- *
11
- * For content-returning ops (fs.read, fs.list, ...) — show the content.
12
- * For mutation ops (fs.write, fs.mkdir, ...) — show a compact tick line.
13
- * For anything else — pass through unchanged.
14
- *
15
- * The silent catch fall-throughs here are correct, not masked failures:
16
- * this is a best-effort display projection where "show the raw string"
17
- * is the honest fallback for anything that doesn't parse.
18
- */
19
- export function formatCapsuleOutput(content: string): string {
20
- const raw = content.trim()
21
-
22
- // Plain JSON array — e.g. fs.list() return value serialised directly
23
- if (raw.startsWith("[")) {
24
- try {
25
- const arr = JSON.parse(raw)
26
- if (Array.isArray(arr)) {
27
- if (arr.length === 0) return "(empty)"
28
- // DirEntry[] — render as a compact directory listing
29
- if (
30
- arr.length > 0 &&
31
- typeof arr[0] === "object" &&
32
- arr[0] !== null &&
33
- "name" in arr[0]
34
- ) {
35
- return arr
36
- .map((e: any) => `${e.type === "directory" ? "d" : "-"} ${e.name}`)
37
- .join("\n")
38
- }
39
- // Generic array of primitives or unknown objects
40
- return arr
41
- .map((e: any) => (typeof e === "object" ? JSON.stringify(e) : String(e)))
42
- .join("\n")
43
- }
44
- } catch {
45
- /* not JSON — fall through to raw */
46
- }
47
- return raw
48
- }
49
-
50
- if (!raw.startsWith("{")) return raw
51
-
52
- // Extract all leading JSON op-record objects.
53
- // Everything after the last op record (the raw auto-logged return value)
54
- // is discarded — the meaningful content comes from the op record's data field.
55
- const opRecords: any[] = []
56
- let i = 0
57
- while (i < raw.length && raw[i] === "{") {
58
- let depth = 0
59
- let j = i
60
- while (j < raw.length) {
61
- const ch = raw[j]
62
- if (ch === '"') {
63
- // skip over string contents, respecting escape sequences
64
- j++
65
- while (j < raw.length) {
66
- if (raw[j] === "\\") {
67
- j += 2
68
- continue
69
- }
70
- if (raw[j] === '"') {
71
- j++
72
- break
73
- }
74
- j++
75
- }
76
- continue
77
- }
78
- if (ch === "{") depth++
79
- else if (ch === "}") {
80
- depth--
81
- if (depth === 0) {
82
- j++
83
- break
84
- }
85
- }
86
- j++
87
- }
88
- try {
89
- const obj = JSON.parse(raw.slice(i, j))
90
- if ("op" in obj) opRecords.push(obj)
91
- else if ("procId" in obj && "command" in obj) {
92
- const tail = obj.tail ? `\n${obj.tail}` : ""
93
- return `spawned ${obj.command} procId=${obj.procId} pid=${obj.pid ?? "?"} status=${obj.status ?? "running"}${tail}`
94
- } else break
95
- } catch {
96
- break
97
- }
98
- i = j
99
- while (i < raw.length && (raw[i] === "\n" || raw[i] === "\r")) i++
100
- }
101
-
102
- if (opRecords.length === 0) return raw
103
-
104
- return opRecords
105
- .map(obj => {
106
- const op: string = obj.op ?? ""
107
- const d = obj.data ?? {}
108
-
109
- if (!obj.ok) {
110
- return `${op} ${d.path ?? ""} ✗ ${obj.error ?? d.message ?? "failed"}`
111
- }
112
-
113
- switch (op) {
114
- case "fs.read":
115
- case "fs.readLines":
116
- return `read ${d.path} ${formatBytes(d.bytes ?? 0)}\n${d.content ?? ""}`
117
- case "fs.list": {
118
- const entries: any[] = d.entries ?? []
119
- const lines = entries
120
- .map((e: any) => {
121
- const prefix = e.type === "directory" ? "d" : "-"
122
- return `${prefix} ${e.name}`
123
- })
124
- .join("\n")
125
- return `list ${d.path} ${entries.length} entries\n${lines}`
126
- }
127
- case "fs.write":
128
- return `write ${d.path} ${formatBytes(d.bytes ?? 0)} ✓`
129
- case "fs.mkdir":
130
- return `mkdir ${d.path} ✓`
131
- case "fs.delete":
132
- return `rm ${d.path} ✓`
133
- case "fs.move":
134
- return `mv ${d.src} → ${d.dest} ✓`
135
- case "fs.copy":
136
- return `cp ${d.src} → ${d.dest} ✓`
137
- case "fs.cd":
138
- return `cd ${d.path} ✓`
139
- default:
140
- return `${op} ${JSON.stringify(d)} ✓`
141
- }
142
- })
143
- .join("\n")
144
- }
@@ -1,85 +0,0 @@
1
- /** Pure string utilities for AIR rendering. */
2
-
3
- /** Escape XML text content (prose, stdout, user messages). */
4
- export function esc(s: string): string {
5
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
6
- }
7
-
8
- /** Escape a trusted value for a double-quoted XML attribute. */
9
- export function escAttr(s: string): string {
10
- return esc(s).replace(/"/g, "&quot;").replace(/'/g, "&apos;")
11
- }
12
-
13
- /** Escape XML inside code blocks — only & and <. Never > (breaks =>, generics). */
14
- export function escCode(s: string): string {
15
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;")
16
- }
17
-
18
- /** Indent every line of s by `spaces` spaces. */
19
- export function indent(s: string, spaces: number): string {
20
- const pad = " ".repeat(spaces)
21
- return s
22
- .split("\n")
23
- .map(line => pad + line)
24
- .join("\n")
25
- }
26
-
27
- export function formatBytes(n: number): string {
28
- return n >= 1024 ? `${(n / 1024).toFixed(1)}K` : `${n}B`
29
- }
30
-
31
- /**
32
- * Normalize code that may contain literal \n or \t escape sequences outside
33
- * of string literals (a common model mistake). Replaces them with real
34
- * whitespace so the timeline shows clean, executable code.
35
- *
36
- * Best-effort heuristic: only acts on \n/\t outside single-quoted,
37
- * double-quoted, or template-literal strings.
38
- */
39
- export function normalizeCode(code: string): string {
40
- // Fast path: no escape sequences at all
41
- if (!code.includes("\\n") && !code.includes("\\t")) return code
42
-
43
- let result = ""
44
- let i = 0
45
- while (i < code.length) {
46
- const c = code[i]
47
- // Track string boundaries to avoid replacing inside strings
48
- if (c === '"' || c === "'" || c === "`") {
49
- const quote = c
50
- result += c
51
- i++
52
- while (i < code.length) {
53
- const sc = code[i]
54
- if (sc === "\\") {
55
- // Keep escape sequences inside strings as-is
56
- result += code[i] + (code[i + 1] ?? "")
57
- i += 2
58
- } else if (sc === quote) {
59
- result += sc
60
- i++
61
- break
62
- } else {
63
- result += sc
64
- i++
65
- }
66
- }
67
- } else if (c === "\\" && i + 1 < code.length) {
68
- const next = code[i + 1]
69
- if (next === "n") {
70
- result += "\n"
71
- i += 2
72
- } else if (next === "t") {
73
- result += "\t"
74
- i += 2
75
- } else {
76
- result += c + next
77
- i += 2
78
- }
79
- } else {
80
- result += c
81
- i++
82
- }
83
- }
84
- return result
85
- }
@@ -1 +0,0 @@
1
- export { repair } from "./repair"
@@ -1,33 +0,0 @@
1
- /**
2
- * Deterministic, pre-parse repair of raw model output.
3
- *
4
- * Scope is deliberately narrow: string-only fixes for mistakes that are
5
- * common and unambiguous to correct. No semantic guessing, no model calls.
6
- * If a fix isn't mechanically certain, leave the text alone — the parser's
7
- * own incomplete/error reporting is the honest fallback, and that goes back
8
- * to the model as an `agent:output:error` for it to correct itself.
9
- *
10
- * Applied to the full buffered response before it reaches the AIR parser.
11
- */
12
-
13
- const KNOWN_TAGS = ["text", "thinking", "typescript", "shell"] as const
14
-
15
- export function repair(raw: string): string {
16
- return normalizeTagCase(raw)
17
- }
18
-
19
- /**
20
- * Lowercase tag names the model emitted in the wrong case
21
- * (`<Text>`, `<TYPESCRIPT>`) — case is not semantically meaningful here,
22
- * so normalizing it can never change intent, only make it parseable.
23
- */
24
- function normalizeTagCase(raw: string): string {
25
- let out = raw
26
- for (const tag of KNOWN_TAGS) {
27
- const open = new RegExp(`<(${tag})(\\s[^>]*)?>`, "gi")
28
- const close = new RegExp(`</(${tag})>`, "gi")
29
- out = out.replace(open, (match, _name, attrs) => `<${tag}${attrs ?? ""}>`)
30
- out = out.replace(close, `</${tag}>`)
31
- }
32
- return out
33
- }
package/src/air/types.ts DELETED
@@ -1,71 +0,0 @@
1
- /**
2
- * AIR — Agent Intermediate Representation.
3
- *
4
- * A general-purpose LLM protocol: hand the renderer Axon's domain (base
5
- * context, declared tools, an event history) and it produces the ordered
6
- * messages a model sees. AIR owns BOTH halves — render (what the model
7
- * sees) and parse (what it emits back) — from one grammar, so they cannot
8
- * drift.
9
- *
10
- * The render boundary is DOMAIN in, messages out: callers pass AxonTool[]
11
- * and AxonEntry[], never AIR's internal render vocabulary. The
12
- * timeline item shapes below are private to render/ — the exhaustive
13
- * AxonEntry → item translation lives there, next to the parser it
14
- * must agree with.
15
- */
16
-
17
- import type { AxonEntry, AxonScope } from "@arcforge/types"
18
-
19
- // ── Messages ─────────────────────────────────────────────────────────────────
20
-
21
- export type AirMessage = {
22
- role: "system" | "user" | "assistant"
23
- content: string
24
- }
25
-
26
- // ── Modes (output grammar) ───────────────────────────────────────────────────
27
-
28
- export type AirModeType = "text" | "typescript" | "shell"
29
-
30
- export type AirMode = {
31
- /** The output type this mode produces. */
32
- type: AirModeType
33
- /** Optional description override. Falls back to the default for the type. */
34
- description?: string
35
- }
36
-
37
- // ── Render input ───────────────────────────────────────────────────────────
38
- //
39
- // DOMAIN in — the caller passes what it already holds. AIR owns every
40
- // translation into protocol shape: tools → <scope> declarations, entries →
41
- // <timeline> items. A cognet curates (which entries, what order, elided how)
42
- // and hands the lists over; it never manufactures AIR-internal types.
43
-
44
- export type AirRenderInput = {
45
- /** Base context — the agent's identity contract. Rendered as <system>. */
46
- base?: string
47
- /** Capsule-implemented globals. Rendered as <scope lang="ts">. */
48
- scope?: AxonScope
49
- /** The event history to render, already curated by the cognet. Rendered as <timeline>. */
50
- history?: readonly AxonEntry[]
51
- }
52
-
53
- // ── Parser output ────────────────────────────────────────────────────────────
54
-
55
- /**
56
- * Events emitted by the streaming AIR parser.
57
- *
58
- * *:delta — tokens inside a streamable block (<text>, <thinking>), real time.
59
- * *:done — a block closed; content is the full inner text.
60
- * `incomplete: true` means the stream ended without the closing tag —
61
- * callers must treat these as format errors, never as valid actions.
62
- * done — a <done/> self-closing tag was encountered.
63
- */
64
- export type AirBlockEvent =
65
- | { type: "text:delta"; content: string }
66
- | { type: "text:done"; content: string; incomplete?: true }
67
- | { type: "thinking:delta"; content: string }
68
- | { type: "thinking:done"; content: string; incomplete?: true }
69
- | { type: "typescript:done"; content: string; incomplete?: true }
70
- | { type: "shell:done"; content: string; incomplete?: true }
71
- | { type: "done" }