@arcforge/cognet 2.0.97

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.
@@ -0,0 +1,51 @@
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>
@@ -0,0 +1,144 @@
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
+ }
@@ -0,0 +1,85 @@
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
+ }
@@ -0,0 +1 @@
1
+ export { repair } from "./repair"
@@ -0,0 +1,33 @@
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
+ }
@@ -0,0 +1,71 @@
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" }
package/src/clock.ts ADDED
@@ -0,0 +1,118 @@
1
+ import { err } from "@axon/err"
2
+ import type { KernelAbi } from "@arcforge/types"
3
+
4
+ /** Telemetry sink — the cognet's own `kernel.emit`. Fire-and-forget, never awaited. */
5
+ export type ClockEmit = KernelAbi["emit"]
6
+
7
+ export type ClockOpts = {
8
+ emit: ClockEmit
9
+ /**
10
+ * The wake's own abort signal. Lets tick/phase/system tell an intentional
11
+ * interrupt (Escape/Ctrl+C, engine abort) apart from a genuine failure.
12
+ */
13
+ signal?: AbortSignal
14
+ }
15
+
16
+ /**
17
+ * Clock — the world clock and the execution wrappers that drive it.
18
+ *
19
+ * Every cognet has one, whether or not it holds a world: `phase()` and
20
+ * `system()` are ambient globals, and the host wraps each loop iteration in a
21
+ * tick. This is deliberately separate from the ECS so a cognet that never
22
+ * queries an entity — a control loop, a perception stack — doesn't carry an
23
+ * entity store it never touches.
24
+ *
25
+ * tick/phase/system are the ONLY writers of the clock. Each brackets its
26
+ * callback with telemetry so the shape of a thought is recorded as it happens
27
+ * rather than reconstructed afterward.
28
+ *
29
+ * Interrupt is a THIRD outcome, not a failure: when `signal` is aborted,
30
+ * whatever fn() threw is cancellation surfacing through the call stack, not a
31
+ * bug in the phase's own work. Emitting *:failed for that would conflate a
32
+ * cancelled thought with a broken one, and telemetry full of meaningless red
33
+ * is telemetry nobody reads. Real failures still emit *:failed and rethrow —
34
+ * telemetry never swallows.
35
+ */
36
+ export function Clock(opts: ClockOpts) {
37
+ const { emit, signal } = opts
38
+
39
+ let tick = 0
40
+ let phase: string | null = null
41
+
42
+ /** tick/phase stamp merged into every event payload below tick level. */
43
+ function stamp() {
44
+ return { tick, phase }
45
+ }
46
+
47
+ return {
48
+ get tick() {
49
+ return tick
50
+ },
51
+ get phase() {
52
+ return phase
53
+ },
54
+ stamp,
55
+
56
+ /** One iteration of the cognitive loop. Advances the clock. */
57
+ async runTick<T>(fn: () => Promise<T>): Promise<T> {
58
+ tick += 1
59
+ const current = tick
60
+ void emit("cognet:tick:start", { tick: current })
61
+ try {
62
+ const result = await fn()
63
+ void emit("cognet:tick:complete", { tick: current })
64
+ return result
65
+ } catch (cause) {
66
+ if (signal?.aborted) {
67
+ void emit("cognet:tick:interrupted", { tick: current })
68
+ throw cause
69
+ }
70
+ const failure = err(cause)
71
+ void emit("cognet:tick:failed", { tick: current, error: failure })
72
+ throw failure
73
+ }
74
+ },
75
+
76
+ /** A named stage within a tick. Sets the current phase for its duration. */
77
+ async runPhase<T>(name: string, fn: () => Promise<T>): Promise<T> {
78
+ phase = name
79
+ void emit("cognet:phase:start", { tick, phase: name })
80
+ try {
81
+ const result = await fn()
82
+ void emit("cognet:phase:complete", { tick, phase: name })
83
+ return result
84
+ } catch (cause) {
85
+ if (signal?.aborted) {
86
+ void emit("cognet:phase:interrupted", { tick, phase: name })
87
+ throw cause
88
+ }
89
+ const failure = err(cause)
90
+ void emit("cognet:phase:failed", { tick, phase: name, error: failure })
91
+ throw failure
92
+ } finally {
93
+ phase = null
94
+ }
95
+ },
96
+
97
+ /** A unit of work within a phase. Timed for the flame graph. */
98
+ async runSystem<T>(name: string, fn: () => Promise<T>): Promise<T> {
99
+ const started = Date.now()
100
+ void emit("cognet:system:start", { ...stamp(), system: name })
101
+ try {
102
+ const result = await fn()
103
+ void emit("cognet:system:complete", { ...stamp(), system: name, durationMs: Date.now() - started })
104
+ return result
105
+ } catch (cause) {
106
+ if (signal?.aborted) {
107
+ void emit("cognet:system:interrupted", { ...stamp(), system: name })
108
+ throw cause
109
+ }
110
+ const failure = err(cause)
111
+ void emit("cognet:system:failed", { ...stamp(), system: name, error: failure })
112
+ throw failure
113
+ }
114
+ },
115
+ }
116
+ }
117
+
118
+ export type ClockT = ReturnType<typeof Clock>
package/src/define.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { CognetDefinition } from "@arcforge/types"
2
+
3
+ /**
4
+ * The cognet authoring surface. Cognet projects (cognets/*) call this in
5
+ * their main.ts; the CLI bundle gets it as an injected global — the
6
+ * desugared form and the global form compile to the same thing, so there
7
+ * is never a side channel around the ABI.
8
+ */
9
+ export function defineCognet(definition: CognetDefinition): CognetDefinition {
10
+ return definition
11
+ }
@@ -0,0 +1,78 @@
1
+ import type { StateT } from "./state"
2
+ import type { ComponentRegistry, ComponentType, EntityId } from "./types"
3
+ import type { EcsEmit } from "./ecs"
4
+
5
+ type ComponentOpts = {
6
+ state: StateT
7
+ emit: EcsEmit
8
+ }
9
+
10
+ /**
11
+ * Component — the single write path for component data.
12
+ * Every write emits kernel telemetry and fires watchers; Entity() delegates
13
+ * here so there is exactly one place a component can change.
14
+ *
15
+ * Telemetry goes to the runtime bus (→ tracing pipeline), never the session
16
+ * log. Bus emit never rejects (handler errors are re-emitted as
17
+ * axon:bus:error), so firing without await keeps writes synchronous.
18
+ */
19
+ export function Component(opts: ComponentOpts) {
20
+ const { state, emit } = opts
21
+
22
+ return {
23
+ add<K extends ComponentType>({
24
+ entity,
25
+ type,
26
+ data,
27
+ }: {
28
+ entity: EntityId
29
+ type: K
30
+ data: ComponentRegistry[K]
31
+ }) {
32
+ let store = state.components.get(type)
33
+ if (!store) {
34
+ store = new Map<EntityId, ComponentRegistry[K]>()
35
+ state.components.set(type, store)
36
+ }
37
+
38
+ const existed = store.has(entity)
39
+ store.set(entity, data)
40
+
41
+ void emit(existed ? "cognet:component:update" : "cognet:component:add", {
42
+ ...state.stamp(),
43
+ entity,
44
+ component: type,
45
+ })
46
+
47
+ state.notify(type, entity, data)
48
+ },
49
+
50
+ remove<K extends ComponentType>({ entity, type }: { entity: EntityId; type: K }) {
51
+ const store = state.components.get(type)
52
+ if (!store?.has(entity)) return
53
+ store.delete(entity)
54
+
55
+ void emit("cognet:component:remove", {
56
+ ...state.stamp(),
57
+ entity,
58
+ component: type,
59
+ })
60
+ },
61
+
62
+ get<K extends ComponentType>({
63
+ entity,
64
+ type,
65
+ }: {
66
+ entity: EntityId
67
+ type: K
68
+ }): ComponentRegistry[K] | undefined {
69
+ return state.components.get(type)?.get(entity)
70
+ },
71
+
72
+ has<K extends ComponentType>({ entity, type }: { entity: EntityId; type: K }): boolean {
73
+ return state.components.get(type)?.has(entity) ?? false
74
+ },
75
+ }
76
+ }
77
+
78
+ export type ComponentT = ReturnType<typeof Component>
package/src/ecs/ecs.ts ADDED
@@ -0,0 +1,51 @@
1
+ import type { KernelAbi } from "@arcforge/types"
2
+ import { Component } from "./component"
3
+ import { Entity } from "./entity"
4
+ import { State } from "./state"
5
+
6
+ /** Telemetry sink — cognets pass abi.emit; fire-and-forget, never awaited. Typed against cognet:*. */
7
+ export type EcsEmit = KernelAbi["emit"]
8
+
9
+ export type EcsOpts = {
10
+ emit: EcsEmit
11
+ /**
12
+ * The clock's stamp. Every world mutation is attributed to the tick and
13
+ * phase it happened in, so the mutation history replays against the clock
14
+ * rather than being a flat list of writes.
15
+ */
16
+ stamp(): { tick: number; phase: string | null }
17
+ }
18
+
19
+ /**
20
+ * Ecs — the wake-scoped world: entities, components, and queries over both.
21
+ *
22
+ * A WORKING SET, not memory. Persistence lives in the session log and durable
23
+ * cognitive state in kernel.store; if an entity matters beyond this wake, it
24
+ * was derived from the log and the next wake derives it again.
25
+ *
26
+ * Deliberately opt-in. The world clock (tick/phase/system) is separate and
27
+ * always present — see ../clock.ts — so a control loop or perception stack
28
+ * that never queries an entity carries none of this.
29
+ *
30
+ * State() owns the store; Component() and Entity() are the write paths, and
31
+ * Entity delegates to Component so there is exactly one place a component can
32
+ * change — which is what makes telemetry and watchers reliable rather than
33
+ * best-effort.
34
+ */
35
+ export function Ecs(opts: EcsOpts) {
36
+ const state = State({ stamp: opts.stamp })
37
+
38
+ const component = Component({ state, emit: opts.emit })
39
+ const entity = Entity({ state, component, emit: opts.emit })
40
+
41
+ return {
42
+ state: state,
43
+ entity: entity,
44
+ component: component,
45
+
46
+ query: state.query,
47
+ watch: state.watch,
48
+ }
49
+ }
50
+
51
+ export type EcsT = ReturnType<typeof Ecs>