@dcasegr/skein 0.0.0-stage → 0.1.1
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 +522 -2
- package/bin/skein-host.js +22 -0
- package/bin/skein.js +8 -0
- package/dist/client/admin-cli.d.ts +10 -0
- package/dist/client/admin-cli.js +370 -0
- package/dist/client/admin.d.ts +139 -0
- package/dist/client/admin.js +256 -0
- package/dist/client/bundle.d.ts +9 -0
- package/dist/client/bundle.js +34 -0
- package/dist/client/cli.d.ts +44 -0
- package/dist/client/cli.js +306 -0
- package/dist/client/client.d.ts +161 -0
- package/dist/client/client.js +237 -0
- package/dist/client/config.d.ts +18 -0
- package/dist/client/config.js +31 -0
- package/dist/client/conversation.d.ts +62 -0
- package/dist/client/conversation.js +113 -0
- package/dist/client/raw.d.ts +94 -0
- package/dist/client/raw.js +154 -0
- package/dist/client/target.d.ts +34 -0
- package/dist/client/target.js +158 -0
- package/dist/dev/cli.d.ts +8 -0
- package/dist/dev/cli.js +248 -0
- package/dist/dev/scan.d.ts +15 -0
- package/dist/dev/scan.js +173 -0
- package/dist/envelope-cbor.d.ts +110 -0
- package/dist/envelope-cbor.js +160 -0
- package/dist/envelope.d.ts +38 -0
- package/dist/envelope.js +101 -0
- package/dist/host/arc.d.ts +131 -0
- package/dist/host/arc.js +425 -0
- package/dist/host/billing.d.ts +102 -0
- package/dist/host/billing.js +167 -0
- package/dist/host/boot.d.ts +246 -0
- package/dist/host/boot.js +643 -0
- package/dist/host/cli.d.ts +57 -0
- package/dist/host/cli.js +1253 -0
- package/dist/host/clock.d.ts +3 -0
- package/dist/host/clock.js +8 -0
- package/dist/host/control.d.ts +45 -0
- package/dist/host/control.js +170 -0
- package/dist/host/cron.d.ts +99 -0
- package/dist/host/cron.js +196 -0
- package/dist/host/deliver.d.ts +53 -0
- package/dist/host/deliver.js +251 -0
- package/dist/host/deploy.d.ts +29 -0
- package/dist/host/deploy.js +55 -0
- package/dist/host/door-answer.d.ts +24 -0
- package/dist/host/door-answer.js +21 -0
- package/dist/host/events.d.ts +89 -0
- package/dist/host/events.js +222 -0
- package/dist/host/fake-arcade.d.ts +40 -0
- package/dist/host/fake-arcade.js +149 -0
- package/dist/host/fake-discovery.d.ts +4 -0
- package/dist/host/fake-discovery.js +58 -0
- package/dist/host/feeds.d.ts +127 -0
- package/dist/host/feeds.js +422 -0
- package/dist/host/frontdoor.d.ts +92 -0
- package/dist/host/frontdoor.js +172 -0
- package/dist/host/genesis.d.ts +259 -0
- package/dist/host/genesis.js +353 -0
- package/dist/host/handles.d.ts +40 -0
- package/dist/host/handles.js +88 -0
- package/dist/host/hostenv.d.ts +30 -0
- package/dist/host/hostenv.js +83 -0
- package/dist/host/image-chain.d.ts +127 -0
- package/dist/host/image-chain.js +440 -0
- package/dist/host/install.d.ts +22 -0
- package/dist/host/install.js +139 -0
- package/dist/host/instances.d.ts +227 -0
- package/dist/host/instances.js +430 -0
- package/dist/host/kernel.d.ts +246 -0
- package/dist/host/kernel.js +603 -0
- package/dist/host/liveness.d.ts +65 -0
- package/dist/host/liveness.js +155 -0
- package/dist/host/manifest.d.ts +160 -0
- package/dist/host/manifest.js +597 -0
- package/dist/host/p2p.d.ts +337 -0
- package/dist/host/p2p.js +1002 -0
- package/dist/host/packet.d.ts +121 -0
- package/dist/host/packet.js +562 -0
- package/dist/host/plan.d.ts +154 -0
- package/dist/host/plan.js +418 -0
- package/dist/host/providers.d.ts +238 -0
- package/dist/host/providers.js +565 -0
- package/dist/host/roster.d.ts +72 -0
- package/dist/host/roster.js +172 -0
- package/dist/host/router.d.ts +773 -0
- package/dist/host/router.js +2389 -0
- package/dist/host/signer.d.ts +49 -0
- package/dist/host/signer.js +167 -0
- package/dist/host/store-sqlite.d.ts +39 -0
- package/dist/host/store-sqlite.js +170 -0
- package/dist/host/stores.d.ts +43 -0
- package/dist/host/stores.js +342 -0
- package/dist/host/sync.d.ts +121 -0
- package/dist/host/sync.js +467 -0
- package/dist/host/testbeef.d.ts +21 -0
- package/dist/host/testbeef.js +37 -0
- package/dist/host/testhost.d.ts +149 -0
- package/dist/host/testhost.js +211 -0
- package/dist/host/vcdiff.d.ts +9 -0
- package/dist/host/vcdiff.js +210 -0
- package/dist/host/wasm-kernel.d.ts +87 -0
- package/dist/host/wasm-kernel.js +296 -0
- package/dist/host/wasm-worker.d.ts +1 -0
- package/dist/host/wasm-worker.js +186 -0
- package/dist/peers/cron.d.ts +33 -0
- package/dist/peers/cron.js +75 -0
- package/dist/peers/infer-main.d.ts +2 -0
- package/dist/peers/infer-main.js +74 -0
- package/dist/peers/infer.d.ts +163 -0
- package/dist/peers/infer.js +367 -0
- package/dist/runtime/beef.d.ts +28 -0
- package/dist/runtime/beef.js +107 -0
- package/dist/runtime/bitcoin.d.ts +36 -0
- package/dist/runtime/bitcoin.js +141 -0
- package/dist/runtime/cid.d.ts +14 -0
- package/dist/runtime/cid.js +51 -0
- package/dist/runtime/dispatch.d.ts +92 -0
- package/dist/runtime/dispatch.js +142 -0
- package/dist/runtime/envelope.d.ts +60 -0
- package/dist/runtime/envelope.js +101 -0
- package/dist/runtime/heads.d.ts +25 -0
- package/dist/runtime/heads.js +37 -0
- package/dist/runtime/identity.d.ts +28 -0
- package/dist/runtime/identity.js +67 -0
- package/dist/runtime/index-store.d.ts +152 -0
- package/dist/runtime/index-store.js +715 -0
- package/dist/runtime/log.d.ts +62 -0
- package/dist/runtime/log.js +104 -0
- package/dist/runtime/programs.d.ts +30 -0
- package/dist/runtime/programs.js +61 -0
- package/dist/runtime/records.d.ts +135 -0
- package/dist/runtime/records.js +149 -0
- package/dist/runtime/sqlite.d.ts +33 -0
- package/dist/runtime/sqlite.js +604 -0
- package/dist/runtime/store.d.ts +141 -0
- package/dist/runtime/store.js +24 -0
- package/dist/runtime/syscalls.d.ts +9 -0
- package/dist/runtime/syscalls.js +41 -0
- package/dist/runtime/tree.d.ts +60 -0
- package/dist/runtime/tree.js +196 -0
- package/dist/runtime/types.d.ts +100 -0
- package/dist/runtime/types.js +7 -0
- package/dist/testapps.d.ts +101 -0
- package/dist/testapps.js +275 -0
- package/dist/testkit.d.ts +25 -0
- package/dist/testkit.js +48 -0
- package/dist/wallet.d.ts +35 -0
- package/dist/wallet.js +87 -0
- package/images/README.md +68 -0
- package/images/default/README.md +38 -0
- package/images/default/bin/frontdoor.cid +1 -0
- package/images/default/bin/frontdoor.json +5 -0
- package/images/default/bin/messagebox.cid +1 -0
- package/images/default/bin/messagebox.json +8 -0
- package/images/default/bin/wallet.cid +1 -0
- package/images/default/bin/wallet.json +14 -0
- package/images/default/etc/config.json +1 -0
- package/images/default/etc/dispatch.json +82 -0
- package/images/host/etc/apps.json +8 -0
- package/kernel-zig/README.md +1247 -0
- package/kernel-zig/web/client.js +65 -0
- package/kernel-zig/web/kernel.d.ts +59 -0
- package/kernel-zig/web/kernel.js +329 -0
- package/kernel-zig/web/store.js +67 -0
- package/kernel-zig/web/worker.js +79 -0
- package/kernel-zig/zig-out/web/skein-kernel.wasm +0 -0
- package/package.json +77 -4
- package/wasm/README.md +35 -0
- package/wasm/frontdoor.wasm +0 -0
- package/wasm/messagebox.wasm +0 -0
- package/wasm/resolve.wasm +0 -0
- package/wasm/wallet.wasm +0 -0
- package/wasm/wire-probe.wasm +0 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { WalletInterface } from "@bsv/sdk";
|
|
2
|
+
import { CID } from "multiformats/cid";
|
|
3
|
+
import type { Listed as RawListed } from "../client/raw.ts";
|
|
4
|
+
/** One listed message: BRC-33 fields as the messagebox reports them. */
|
|
5
|
+
export interface Listed {
|
|
6
|
+
messageId: string;
|
|
7
|
+
sender?: string;
|
|
8
|
+
body: unknown;
|
|
9
|
+
created_at?: string;
|
|
10
|
+
}
|
|
11
|
+
/** The part of a BRC-33 messagebox a peer uses, as one identity. */
|
|
12
|
+
export interface MessageBox {
|
|
13
|
+
list(box: string): Promise<Listed[]>;
|
|
14
|
+
ack(ids: string[]): Promise<void>;
|
|
15
|
+
send(m: {
|
|
16
|
+
recipient: string;
|
|
17
|
+
box: string;
|
|
18
|
+
body: object;
|
|
19
|
+
}): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
/** The raw transport (#40): its own mailbox, a session per peer messagebox, its address book. */
|
|
22
|
+
export interface RawTransport {
|
|
23
|
+
/** Its mailbox instance (a RawBox). */
|
|
24
|
+
inbox: {
|
|
25
|
+
list(box: string): Promise<RawListed[]>;
|
|
26
|
+
ack(ids: string[]): Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
/** A session with the messagebox at `url` (a RawBox). */
|
|
29
|
+
outbox(url: string): {
|
|
30
|
+
send(recipient: string, box: string, body: unknown): Promise<unknown>;
|
|
31
|
+
};
|
|
32
|
+
/** Its address book (#40): the messagebox URL of the sender `key`, configured by its admin; undefined: no route. */
|
|
33
|
+
addressOf(key: string): string | undefined;
|
|
34
|
+
}
|
|
35
|
+
export interface Provider {
|
|
36
|
+
baseUrl: string;
|
|
37
|
+
apiKey?: string;
|
|
38
|
+
}
|
|
39
|
+
export type Thinking = "off" | "low" | "medium" | "high";
|
|
40
|
+
/** One engine call: the whole chat. */
|
|
41
|
+
export interface EngineRequest {
|
|
42
|
+
model: string;
|
|
43
|
+
messages: unknown[];
|
|
44
|
+
tools?: unknown[];
|
|
45
|
+
thinking?: Thinking;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A conversation node: one of the loop's `turn` records (the loop: shruggr/skein-chat programs/loop), as
|
|
49
|
+
* kept in the instance, so its CID here is its CID there. `parent` is the node
|
|
50
|
+
* before it; the root (the system prompt) has none.
|
|
51
|
+
*/
|
|
52
|
+
export interface TurnNode {
|
|
53
|
+
kind: "turn";
|
|
54
|
+
parent?: CID;
|
|
55
|
+
role: string;
|
|
56
|
+
text?: string;
|
|
57
|
+
content?: string;
|
|
58
|
+
tool_calls?: unknown[];
|
|
59
|
+
call?: string;
|
|
60
|
+
exitCode?: number;
|
|
61
|
+
stdout?: string;
|
|
62
|
+
stderr?: string;
|
|
63
|
+
error?: string;
|
|
64
|
+
[k: string]: unknown;
|
|
65
|
+
}
|
|
66
|
+
/** An `infer` body: the nodes new since the sender's last request, and the node they extend. */
|
|
67
|
+
export interface InferRequest {
|
|
68
|
+
model: string;
|
|
69
|
+
thinking?: Thinking;
|
|
70
|
+
tools?: unknown[];
|
|
71
|
+
/** The node `nodes[0]` extends; absent when the nodes start at the root. */
|
|
72
|
+
parent?: CID;
|
|
73
|
+
nodes?: TurnNode[];
|
|
74
|
+
/** Before #12: the whole chat, passed through. */
|
|
75
|
+
messages?: unknown[];
|
|
76
|
+
}
|
|
77
|
+
export type Completion = {
|
|
78
|
+
message: {
|
|
79
|
+
role: "assistant";
|
|
80
|
+
content?: string;
|
|
81
|
+
reasoning?: string;
|
|
82
|
+
tool_calls?: unknown[];
|
|
83
|
+
};
|
|
84
|
+
usage?: unknown;
|
|
85
|
+
model: string;
|
|
86
|
+
ms: number;
|
|
87
|
+
} | {
|
|
88
|
+
missing: CID[];
|
|
89
|
+
} | {
|
|
90
|
+
error: string;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* The peer's side of the conversation graph: nodes by CID, per sender (one
|
|
94
|
+
* sender's nodes never reach another's chat). In memory, the `max` most
|
|
95
|
+
* recently used; with a `dir`, every node is also on disk as
|
|
96
|
+
* `<dir>/<sender>/<cid>` (its dag-cbor bytes) and read back after a restart or
|
|
97
|
+
* an eviction. Nothing prunes the directory.
|
|
98
|
+
*/
|
|
99
|
+
export declare class NodeGraph {
|
|
100
|
+
private readonly mem;
|
|
101
|
+
private readonly o;
|
|
102
|
+
constructor(o?: {
|
|
103
|
+
dir?: string;
|
|
104
|
+
max?: number;
|
|
105
|
+
});
|
|
106
|
+
get(sender: string, cid: CID): TurnNode | undefined;
|
|
107
|
+
put(sender: string, cid: CID, node: TurnNode, bytes: Uint8Array): void;
|
|
108
|
+
/** Drop everything held in memory, as a restart would; the disk stays. */
|
|
109
|
+
forget(): void;
|
|
110
|
+
get size(): number;
|
|
111
|
+
private remember;
|
|
112
|
+
}
|
|
113
|
+
export interface InferOptions {
|
|
114
|
+
wallet: WalletInterface;
|
|
115
|
+
/** The envelope transport: BRC-169 envelopes over a BRC-33 messagebox. */
|
|
116
|
+
box?: MessageBox;
|
|
117
|
+
/** The raw transport (#40). */
|
|
118
|
+
raw?: RawTransport;
|
|
119
|
+
providers: Record<string, Provider>;
|
|
120
|
+
/** The conversation graph. Default: in memory only. */
|
|
121
|
+
graph?: NodeGraph;
|
|
122
|
+
/** Default: global fetch. Tests script it. */
|
|
123
|
+
fetch?: typeof fetch;
|
|
124
|
+
/** This peer's handle, for the envelopes it seals. Default infer@localhost. */
|
|
125
|
+
handle?: string;
|
|
126
|
+
domain?: string;
|
|
127
|
+
/** Accept `created` within ± this of now. Default 10 minutes. */
|
|
128
|
+
freshnessMs?: number;
|
|
129
|
+
now?: () => number;
|
|
130
|
+
log?: (line: string) => void;
|
|
131
|
+
}
|
|
132
|
+
export declare class InferPeer {
|
|
133
|
+
private readonly o;
|
|
134
|
+
readonly graph: NodeGraph;
|
|
135
|
+
private readonly say;
|
|
136
|
+
private timer?;
|
|
137
|
+
private polling?;
|
|
138
|
+
constructor(o: InferOptions);
|
|
139
|
+
start(ms?: number): void;
|
|
140
|
+
stop(): Promise<void>;
|
|
141
|
+
/** Collect the `infer` box once: answer each request, acknowledge it. Returns how many were answered. */
|
|
142
|
+
poll(): Promise<number>;
|
|
143
|
+
/** The raw transport (#40): each request answered at the messagebox the address book names for its sender. */
|
|
144
|
+
private pollRaw;
|
|
145
|
+
/** One request: screen, open, complete, reply. False if rejected (logged). */
|
|
146
|
+
handle(m: Listed): Promise<boolean>;
|
|
147
|
+
/**
|
|
148
|
+
* One request from `sender`: take its nodes into the graph, walk back from
|
|
149
|
+
* the newest to the root, and complete the chat that path is. A node it
|
|
150
|
+
* cannot find — the named parent, or an ancestor it no longer holds — is a
|
|
151
|
+
* `missing` reply naming it, and no engine call.
|
|
152
|
+
*/
|
|
153
|
+
answer(sender: string, req: InferRequest): Promise<Completion>;
|
|
154
|
+
/** Call the provider the model names ("<provider>/<model>") with the whole chat. */
|
|
155
|
+
complete(req: EngineRequest): Promise<Completion>;
|
|
156
|
+
/** Answer in the form the request came in (a JSON-form request gets a JSON-form reply). */
|
|
157
|
+
private reply;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* A path of turns as OpenAI chat messages: system, user, assistant and tool
|
|
161
|
+
* turns in order; the others (errors) are the loop's records, not the model's.
|
|
162
|
+
*/
|
|
163
|
+
export declare function chat(path: TurnNode[]): unknown[];
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
// The inference peer (docs/ARCH.md, "Everything outside is a peer"): its own
|
|
2
|
+
// identity and wallet, a process of its own. It collects its `infer` box on
|
|
3
|
+
// the messagebox, verifies and opens each BRC-169 envelope with its wallet
|
|
4
|
+
// (open() checks the plaintext against the sender-signed contentHash), calls an
|
|
5
|
+
// OpenAI-compatible endpoint, and seals the completion back to the sender in
|
|
6
|
+
// `completions` with `replyTo` = the request's id, its signed part's CID (which
|
|
7
|
+
// is how the runtime delivers it to the loop thread awaiting it). It is the
|
|
8
|
+
// only thing in skein that fetches for inference.
|
|
9
|
+
//
|
|
10
|
+
// It is stateful (issue #12; docs/MESSAGES.md, "The infer protocol"): it holds
|
|
11
|
+
// each sender's conversation graph — the loop's `turn` records, keyed by CID,
|
|
12
|
+
// each naming its `parent` — so a request carries only the nodes that are new
|
|
13
|
+
// since the last one and names the node they extend. The engine behind it is
|
|
14
|
+
// stateless: the peer walks the graph back from the newest node to the root
|
|
15
|
+
// and sends that whole path as OpenAI chat messages.
|
|
16
|
+
//
|
|
17
|
+
// infer {model: "<provider>/<model>", thinking?: "off"|"low"|"medium"|"high", tools?,
|
|
18
|
+
// parent?: <node cid>, nodes: [<turn>…]}
|
|
19
|
+
// completions {replyTo, message: {role: "assistant", content?, reasoning?, tool_calls?}, usage?, model, ms}
|
|
20
|
+
// | {replyTo, missing: [<node cid>]} (the peer does not hold that node: resend)
|
|
21
|
+
// | {replyTo, error}
|
|
22
|
+
//
|
|
23
|
+
// (A request with `messages` and no `nodes` — a loop from before #12 — is
|
|
24
|
+
// passed through as it is.)
|
|
25
|
+
//
|
|
26
|
+
// Providers come from ~/.skein/infer.json: {"ripper": {"baseUrl": "http://…/v1", "apiKey": "…"}}.
|
|
27
|
+
//
|
|
28
|
+
// Two transports. Since #40 (`raw`): its mailbox is a mailbox instance at its
|
|
29
|
+
// own origin, and it speaks raw BRC-33 on BRC-104 sessions — it lists its
|
|
30
|
+
// `infer` box there (a request's id is its messageId: the record the instance
|
|
31
|
+
// keeps; the sender is the session's identity), and answers in the sender's
|
|
32
|
+
// `completions` at the sender's own messagebox. Where that is comes from its
|
|
33
|
+
// address book (key → messagebox URL), which its admin configures
|
|
34
|
+
// (skein-infer: SKEIN_INFER_PEERS; scripts/host/up.sh writes every agent's
|
|
35
|
+
// there). Nobody registers with it: a request from a key not in the address
|
|
36
|
+
// book has nowhere to go, and is dropped with one line saying so. The
|
|
37
|
+
// envelope transport (`box`): BRC-169 envelopes (§7.2, JSON) over any BRC-33
|
|
38
|
+
// messagebox, one listed message per request.
|
|
39
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
40
|
+
import { join } from "node:path";
|
|
41
|
+
import * as dagCbor from "@ipld/dag-cbor";
|
|
42
|
+
import { CID } from "multiformats/cid";
|
|
43
|
+
import { isoTime, open, seal } from "../envelope.js";
|
|
44
|
+
import { asEnvelope, inspect, isCborEnvelope, openCbor, sealCbor, wrapCbor } from "../envelope-cbor.js";
|
|
45
|
+
import { encode } from "../runtime/cid.js";
|
|
46
|
+
const THINKING = ["off", "low", "medium", "high"];
|
|
47
|
+
/**
|
|
48
|
+
* The peer's side of the conversation graph: nodes by CID, per sender (one
|
|
49
|
+
* sender's nodes never reach another's chat). In memory, the `max` most
|
|
50
|
+
* recently used; with a `dir`, every node is also on disk as
|
|
51
|
+
* `<dir>/<sender>/<cid>` (its dag-cbor bytes) and read back after a restart or
|
|
52
|
+
* an eviction. Nothing prunes the directory.
|
|
53
|
+
*/
|
|
54
|
+
export class NodeGraph {
|
|
55
|
+
mem = new Map();
|
|
56
|
+
o;
|
|
57
|
+
constructor(o = {}) {
|
|
58
|
+
this.o = o;
|
|
59
|
+
if (o.dir)
|
|
60
|
+
mkdirSync(o.dir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
get(sender, cid) {
|
|
63
|
+
const k = `${sender}/${cid}`;
|
|
64
|
+
const hit = this.mem.get(k);
|
|
65
|
+
if (hit) {
|
|
66
|
+
this.mem.delete(k);
|
|
67
|
+
this.mem.set(k, hit);
|
|
68
|
+
return hit;
|
|
69
|
+
}
|
|
70
|
+
if (!this.o.dir)
|
|
71
|
+
return undefined;
|
|
72
|
+
let bytes;
|
|
73
|
+
try {
|
|
74
|
+
bytes = readFileSync(join(this.o.dir, sender, cid.toString()));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const node = dagCbor.decode(bytes);
|
|
80
|
+
if (!encode(node).cid.equals(cid))
|
|
81
|
+
return undefined; // a damaged file is a missing node
|
|
82
|
+
this.remember(k, node);
|
|
83
|
+
return node;
|
|
84
|
+
}
|
|
85
|
+
put(sender, cid, node, bytes) {
|
|
86
|
+
const k = `${sender}/${cid}`;
|
|
87
|
+
if (this.mem.has(k))
|
|
88
|
+
return;
|
|
89
|
+
this.remember(k, node);
|
|
90
|
+
if (this.o.dir) {
|
|
91
|
+
const d = join(this.o.dir, sender);
|
|
92
|
+
mkdirSync(d, { recursive: true });
|
|
93
|
+
writeFileSync(join(d, cid.toString()), bytes);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Drop everything held in memory, as a restart would; the disk stays. */
|
|
97
|
+
forget() { this.mem.clear(); }
|
|
98
|
+
get size() { return this.mem.size; }
|
|
99
|
+
remember(k, node) {
|
|
100
|
+
this.mem.set(k, node);
|
|
101
|
+
const max = this.o.max ?? 50_000;
|
|
102
|
+
for (const old of this.mem.keys()) {
|
|
103
|
+
if (this.mem.size <= max)
|
|
104
|
+
break;
|
|
105
|
+
this.mem.delete(old);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const short = (s) => s.slice(-8);
|
|
110
|
+
export class InferPeer {
|
|
111
|
+
o;
|
|
112
|
+
graph;
|
|
113
|
+
say;
|
|
114
|
+
timer;
|
|
115
|
+
polling;
|
|
116
|
+
constructor(o) {
|
|
117
|
+
this.o = o;
|
|
118
|
+
this.say = o.log ?? (() => { });
|
|
119
|
+
this.graph = o.graph ?? new NodeGraph();
|
|
120
|
+
}
|
|
121
|
+
start(ms = 1000) {
|
|
122
|
+
const tick = () => { if (!this.polling)
|
|
123
|
+
this.polling = this.poll().catch((e) => this.say(`infer: ${e.message}`)).finally(() => { this.polling = undefined; }); };
|
|
124
|
+
tick();
|
|
125
|
+
this.timer = setInterval(tick, ms);
|
|
126
|
+
}
|
|
127
|
+
async stop() {
|
|
128
|
+
if (this.timer)
|
|
129
|
+
clearInterval(this.timer);
|
|
130
|
+
await this.polling;
|
|
131
|
+
}
|
|
132
|
+
/** Collect the `infer` box once: answer each request, acknowledge it. Returns how many were answered. */
|
|
133
|
+
async poll() {
|
|
134
|
+
if (this.o.raw)
|
|
135
|
+
return this.pollRaw(this.o.raw);
|
|
136
|
+
let n = 0;
|
|
137
|
+
for (const m of await this.o.box.list("infer")) {
|
|
138
|
+
try {
|
|
139
|
+
if (await this.handle(m))
|
|
140
|
+
n++;
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
this.say(`infer ${m.messageId}: ${e.message} (left unacknowledged)`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
await this.o.box.ack([m.messageId]);
|
|
147
|
+
}
|
|
148
|
+
return n;
|
|
149
|
+
}
|
|
150
|
+
/** The raw transport (#40): each request answered at the messagebox the address book names for its sender. */
|
|
151
|
+
async pollRaw(raw) {
|
|
152
|
+
let n = 0;
|
|
153
|
+
for (const m of await raw.inbox.list("infer")) {
|
|
154
|
+
const url = raw.addressOf(m.sender);
|
|
155
|
+
if (!url) {
|
|
156
|
+
this.say(`infer ${short(m.messageId)} from ${short(m.sender)}: no route: ${m.sender} is not in the address book; dropped`);
|
|
157
|
+
await raw.inbox.ack([m.messageId]);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const replyTo = CID.parse(m.messageId);
|
|
161
|
+
let result;
|
|
162
|
+
try {
|
|
163
|
+
result = await this.answer(m.sender, m.value);
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
result = { error: e.message };
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
await raw.outbox(url).send(m.sender, "completions", clean({ replyTo, ...result }));
|
|
170
|
+
n++;
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
this.say(`infer ${short(m.messageId)}: answer to ${url}: ${e.message} (left unacknowledged)`);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
this.say(`infer ${short(m.messageId)} from ${short(m.sender)}: ${"error" in result ? `error: ${result.error}` : "missing" in result ? `missing ${result.missing.map((c) => short(c.toString())).join(", ")}` : `${result.model} ${result.ms} ms`}`);
|
|
177
|
+
await raw.inbox.ack([m.messageId]);
|
|
178
|
+
}
|
|
179
|
+
return n;
|
|
180
|
+
}
|
|
181
|
+
/** One request: screen, open, complete, reply. False if rejected (logged). */
|
|
182
|
+
async handle(m) {
|
|
183
|
+
const reject = (why) => { this.say(`infer ${m.messageId}: rejected: ${why}`); return false; };
|
|
184
|
+
// Either envelope form (issue #33): §7.2 JSON, or §7.3 dag-cbor (BRC-231 bytes, or {"dag-cbor": base64} in a JSON listing).
|
|
185
|
+
const env = asEnvelope(m.body);
|
|
186
|
+
if (!env)
|
|
187
|
+
return reject("not a BRC-169 envelope");
|
|
188
|
+
let x;
|
|
189
|
+
try {
|
|
190
|
+
x = inspect(env);
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
return reject(e.message);
|
|
194
|
+
}
|
|
195
|
+
if (!x.verified)
|
|
196
|
+
return reject("not a verifiable BRC-169 envelope");
|
|
197
|
+
if (m.sender !== undefined && m.sender !== x.sender)
|
|
198
|
+
return reject("messagebox sender is not envelope.sender");
|
|
199
|
+
const now = (this.o.now ?? Date.now)();
|
|
200
|
+
if (Math.abs(now - Date.parse(env.created)) > (this.o.freshnessMs ?? 10 * 60_000))
|
|
201
|
+
return reject(`created ${env.created} is not fresh`);
|
|
202
|
+
const replyTo = x.id;
|
|
203
|
+
let result;
|
|
204
|
+
try {
|
|
205
|
+
const plain = isCborEnvelope(env) ? await openCbor(this.o.wallet, env) : await open(this.o.wallet, env);
|
|
206
|
+
const req = dagCbor.decode(plain.body);
|
|
207
|
+
result = await this.answer(x.sender, req);
|
|
208
|
+
}
|
|
209
|
+
catch (e) {
|
|
210
|
+
result = { error: e.message };
|
|
211
|
+
}
|
|
212
|
+
await this.reply(env, x.sender, { replyTo, ...result });
|
|
213
|
+
this.say(`infer ${short(replyTo.toString())} from ${short(x.sender)}: ${"error" in result ? `error: ${result.error}` : "missing" in result ? `missing ${result.missing.map((c) => short(c.toString())).join(", ")}` : `${result.model} ${result.ms} ms${result.message.tool_calls?.length ? ` · ${result.message.tool_calls.length} tool call(s)` : ""}`}`);
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* One request from `sender`: take its nodes into the graph, walk back from
|
|
218
|
+
* the newest to the root, and complete the chat that path is. A node it
|
|
219
|
+
* cannot find — the named parent, or an ancestor it no longer holds — is a
|
|
220
|
+
* `missing` reply naming it, and no engine call.
|
|
221
|
+
*/
|
|
222
|
+
async answer(sender, req) {
|
|
223
|
+
const wants = "infer wants {model, thinking?, tools?, parent?, nodes}";
|
|
224
|
+
if (typeof req?.model !== "string")
|
|
225
|
+
return { error: wants };
|
|
226
|
+
if (req.nodes === undefined && Array.isArray(req.messages))
|
|
227
|
+
return this.complete(req);
|
|
228
|
+
if (!Array.isArray(req.nodes) || req.nodes.length === 0)
|
|
229
|
+
return { error: `${wants}: no nodes` };
|
|
230
|
+
const parent = req.parent === undefined ? undefined : CID.asCID(req.parent) ?? undefined;
|
|
231
|
+
if (req.parent !== undefined && !parent)
|
|
232
|
+
return { error: "infer: parent is not a CID" };
|
|
233
|
+
let prev = parent;
|
|
234
|
+
const blocks = [];
|
|
235
|
+
for (const [i, node] of req.nodes.entries()) {
|
|
236
|
+
if (!node || typeof node !== "object" || node.kind !== "turn" || typeof node.role !== "string")
|
|
237
|
+
return { error: `infer: node ${i} is not a turn` };
|
|
238
|
+
const up = node.parent === undefined ? undefined : CID.asCID(node.parent) ?? undefined;
|
|
239
|
+
if (!(up === prev || (up && prev && up.equals(prev))))
|
|
240
|
+
return { error: `infer: node ${i} does not extend ${i === 0 ? "the named parent" : `node ${i - 1}`}` };
|
|
241
|
+
const b = encode(node);
|
|
242
|
+
blocks.push({ node, ...b });
|
|
243
|
+
prev = b.cid;
|
|
244
|
+
}
|
|
245
|
+
for (const b of blocks)
|
|
246
|
+
this.graph.put(sender, b.cid, b.node, b.bytes);
|
|
247
|
+
// The path to the newest node, root first. What is not held is missing.
|
|
248
|
+
const path = [];
|
|
249
|
+
for (let c = prev; c;) {
|
|
250
|
+
const n = this.graph.get(sender, c);
|
|
251
|
+
if (!n)
|
|
252
|
+
return { missing: [c] };
|
|
253
|
+
path.push(n);
|
|
254
|
+
c = n.parent === undefined ? undefined : CID.asCID(n.parent) ?? undefined;
|
|
255
|
+
}
|
|
256
|
+
path.reverse();
|
|
257
|
+
return this.complete({ model: req.model, thinking: req.thinking, tools: req.tools, messages: chat(path) });
|
|
258
|
+
}
|
|
259
|
+
/** Call the provider the model names ("<provider>/<model>") with the whole chat. */
|
|
260
|
+
async complete(req) {
|
|
261
|
+
if (typeof req?.model !== "string" || !Array.isArray(req.messages))
|
|
262
|
+
return { error: "infer wants {model, messages, tools?, thinking?}" };
|
|
263
|
+
if (req.thinking !== undefined && !THINKING.includes(req.thinking))
|
|
264
|
+
return { error: `thinking must be one of ${THINKING.join(", ")}, not ${JSON.stringify(req.thinking)}` };
|
|
265
|
+
const slash = req.model.indexOf("/");
|
|
266
|
+
const [name, model] = slash > 0 ? [req.model.slice(0, slash), req.model.slice(slash + 1)] : [req.model, req.model];
|
|
267
|
+
const p = this.o.providers[name];
|
|
268
|
+
if (!p)
|
|
269
|
+
return { error: `no provider "${name}" (known: ${Object.keys(this.o.providers).join(", ") || "none"})` };
|
|
270
|
+
const body = { model, messages: req.messages, stream: false };
|
|
271
|
+
if (req.tools?.length)
|
|
272
|
+
body.tools = req.tools;
|
|
273
|
+
if (req.thinking === "off")
|
|
274
|
+
body.chat_template_kwargs = { enable_thinking: false };
|
|
275
|
+
else if (req.thinking) {
|
|
276
|
+
body.chat_template_kwargs = { enable_thinking: true };
|
|
277
|
+
body.reasoning_effort = req.thinking;
|
|
278
|
+
}
|
|
279
|
+
const t0 = Date.now();
|
|
280
|
+
const res = await (this.o.fetch ?? fetch)(`${p.baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
281
|
+
method: "POST",
|
|
282
|
+
headers: { "content-type": "application/json", ...(p.apiKey ? { authorization: `Bearer ${p.apiKey}` } : {}) },
|
|
283
|
+
body: JSON.stringify(body),
|
|
284
|
+
});
|
|
285
|
+
const text = await res.text();
|
|
286
|
+
if (!res.ok)
|
|
287
|
+
return { error: `${name}: HTTP ${res.status} ${text.slice(0, 500)}` };
|
|
288
|
+
const j = JSON.parse(text);
|
|
289
|
+
const msg = j.choices?.[0]?.message;
|
|
290
|
+
if (!msg)
|
|
291
|
+
return { error: `${name}: no choices in the response` };
|
|
292
|
+
const reasoning = msg.reasoning_content ?? msg.reasoning;
|
|
293
|
+
const calls = Array.isArray(msg.tool_calls) && msg.tool_calls.length ? msg.tool_calls : undefined;
|
|
294
|
+
return clean({
|
|
295
|
+
message: { role: "assistant", content: typeof msg.content === "string" ? msg.content : undefined, reasoning: typeof reasoning === "string" && reasoning ? reasoning : undefined, tool_calls: calls },
|
|
296
|
+
usage: j.usage,
|
|
297
|
+
model: req.model,
|
|
298
|
+
ms: Date.now() - t0,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
/** Answer in the form the request came in (a JSON-form request gets a JSON-form reply). */
|
|
302
|
+
async reply(to, sender, body) {
|
|
303
|
+
const [user, domain] = (this.o.handle ?? "infer@localhost").split("@");
|
|
304
|
+
const args = {
|
|
305
|
+
recipient: { identityKey: sender, handle: to.sender.handle ?? sender.slice(0, 16), domain: to.sender.domain ?? domain ?? "localhost" },
|
|
306
|
+
sender: { handle: user, domain: this.o.domain ?? domain ?? "localhost" },
|
|
307
|
+
body: dagCbor.encode(clean(body)),
|
|
308
|
+
created: isoTime((this.o.now ?? Date.now)()),
|
|
309
|
+
};
|
|
310
|
+
const out = isCborEnvelope(to) ? wrapCbor(await sealCbor(this.o.wallet, args)) : await seal(this.o.wallet, args);
|
|
311
|
+
await this.o.box.send({ recipient: sender, box: "completions", body: out });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* A path of turns as OpenAI chat messages: system, user, assistant and tool
|
|
316
|
+
* turns in order; the others (errors) are the loop's records, not the model's.
|
|
317
|
+
*/
|
|
318
|
+
export function chat(path) {
|
|
319
|
+
const out = [];
|
|
320
|
+
for (const t of path) {
|
|
321
|
+
switch (t.role) {
|
|
322
|
+
case "system":
|
|
323
|
+
out.push({ role: "system", content: t.content ?? "" });
|
|
324
|
+
break;
|
|
325
|
+
case "user":
|
|
326
|
+
out.push({ role: "user", content: userText(t) });
|
|
327
|
+
break;
|
|
328
|
+
case "assistant":
|
|
329
|
+
out.push(clean({ role: "assistant", content: t.content ?? "", tool_calls: t.tool_calls?.length ? t.tool_calls : undefined }));
|
|
330
|
+
break;
|
|
331
|
+
case "tool":
|
|
332
|
+
out.push({ role: "tool", tool_call_id: t.call ?? "", content: toolText(t) });
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
/** A user turn as the model reads it: the text, then the notes it made on presented pages (issue #19), each naming the page by CID. */
|
|
339
|
+
function userText(t) {
|
|
340
|
+
const notes = Array.isArray(t.annotations) ? t.annotations : [];
|
|
341
|
+
if (!notes.length)
|
|
342
|
+
return t.text ?? "";
|
|
343
|
+
const lines = notes.map((a) => `- on ${CID.asCID(a.present)?.toString() ?? String(a.present)}${typeof a.block === "string" && a.block ? ` block ${a.block}` : ""}: ${String(a.note ?? "")}`);
|
|
344
|
+
return `${t.text ?? ""}\n\n[annotations]\n${lines.join("\n")}`;
|
|
345
|
+
}
|
|
346
|
+
/** A tool turn as the model reads it: a message's answer (or what went wrong), or a shell's exit, stdout and stderr. */
|
|
347
|
+
function toolText(t) {
|
|
348
|
+
if (t.exitCode === undefined)
|
|
349
|
+
return t.error ? `error: ${t.error}` : t.text ?? "";
|
|
350
|
+
let s = `exit ${t.exitCode}\n${t.stdout ?? ""}`;
|
|
351
|
+
if (t.stderr)
|
|
352
|
+
s += `\n[stderr]\n${t.stderr}`;
|
|
353
|
+
return s;
|
|
354
|
+
}
|
|
355
|
+
/** Drop undefined and null members (dag-cbor has no undefined; the loop wants absent, not null). */
|
|
356
|
+
function clean(v) {
|
|
357
|
+
if (Array.isArray(v))
|
|
358
|
+
return v.map(clean);
|
|
359
|
+
if (v && typeof v === "object" && Object.getPrototypeOf(v) === Object.prototype) {
|
|
360
|
+
const out = {};
|
|
361
|
+
for (const [k, x] of Object.entries(v))
|
|
362
|
+
if (x !== undefined && x !== null)
|
|
363
|
+
out[k] = clean(x);
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
366
|
+
return v;
|
|
367
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { CID } from "multiformats/cid";
|
|
2
|
+
export type BeefForm = "beef" | "atomic" | "outpoint" | "subject";
|
|
3
|
+
export interface BeefEnvelope {
|
|
4
|
+
form: BeefForm;
|
|
5
|
+
beef: CID;
|
|
6
|
+
subject?: CID;
|
|
7
|
+
vout?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface BeefRecord {
|
|
10
|
+
kind: "beef";
|
|
11
|
+
version: 1 | 2;
|
|
12
|
+
txs: CID[];
|
|
13
|
+
marks: Array<number | null | "txid">;
|
|
14
|
+
bumps: Array<{
|
|
15
|
+
height: number;
|
|
16
|
+
path: CID;
|
|
17
|
+
block: CID | null;
|
|
18
|
+
proves: number[];
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
export declare function isBeefRecord(x: unknown): x is BeefRecord;
|
|
22
|
+
export declare function isBeefEnvelope(x: unknown): x is BeefEnvelope;
|
|
23
|
+
/** The transaction a BEEF is about: an enveloped form's subject, else the record's last transaction. */
|
|
24
|
+
export declare function subjectOf(e: BeefEnvelope, r: BeefRecord): CID | undefined;
|
|
25
|
+
/** The BEEF a pointer record stands for (no envelope); `bytes` reads a block (a transaction, a BUMP's bytes). */
|
|
26
|
+
export declare function beefOf(r: BeefRecord, bytes: (cid: CID) => Promise<Uint8Array>): Promise<Uint8Array>;
|
|
27
|
+
/** The wire bytes an envelope stands for: its prefix, then the BEEF its record stands for. */
|
|
28
|
+
export declare function wireOf(e: BeefEnvelope, r: BeefRecord, bytes: (cid: CID) => Promise<Uint8Array>): Promise<Uint8Array>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// The BEEF envelope and pointer record (#121, #146), read on the host's side:
|
|
2
|
+
// the exact wire bytes back from what the kernel's door wrote — the envelope
|
|
3
|
+
// where the bytes were, the pointer record it links, and the blocks that names
|
|
4
|
+
// (kernel-zig/src/beef.zig `wire`; skein-sdk `chain.record.wireOf` for
|
|
5
|
+
// programs). The door is lossless for anything a signature covers: a reader
|
|
6
|
+
// of the log puts the bytes back and re-checks the signature over them.
|
|
7
|
+
//
|
|
8
|
+
// The envelope is not BEEF (#146): it sits where the bytes were, beside the
|
|
9
|
+
// link to the pointer record, which is the BEEF alone (two envelopes over the
|
|
10
|
+
// same BEEF share one record).
|
|
11
|
+
//
|
|
12
|
+
// envelope {form: "beef" | "atomic" | "outpoint" | "subject", beef: <pointer record CID>,
|
|
13
|
+
// subject?: <bitcoin-tx CID> (an enveloped form's; a bare BEEF's subject is its last
|
|
14
|
+
// transaction), vout?: <an Outpoint BEEF's output>}
|
|
15
|
+
// record {kind: "beef", version: 1 | 2, txs: [<bitcoin-tx CID>], marks: [<bump index> | null | "txid"],
|
|
16
|
+
// bumps: [{height, path: <raw CID: the BUMP as received>, block: <bitcoin-block CID> | null, proves: [<tx index>]}]}
|
|
17
|
+
//
|
|
18
|
+
// The forms' prefixes: Atomic BEEF (BRC-95) 01 01 01 01 ‖ txid; Outpoint BEEF
|
|
19
|
+
// (BRC-158) 16 a7 be ef ‖ txid ‖ vout (u32 LE); Subject BEEF (BRC-233)
|
|
20
|
+
// 57 09 be ef ‖ txid, its BEEF a V2 only.
|
|
21
|
+
import { CID } from "multiformats/cid";
|
|
22
|
+
const FORMS = ["beef", "atomic", "outpoint", "subject"];
|
|
23
|
+
export function isBeefRecord(x) {
|
|
24
|
+
const r = x;
|
|
25
|
+
return !!r && typeof r === "object" && r.kind === "beef" && Array.isArray(r.txs) && Array.isArray(r.marks) && Array.isArray(r.bumps);
|
|
26
|
+
}
|
|
27
|
+
export function isBeefEnvelope(x) {
|
|
28
|
+
const e = x;
|
|
29
|
+
return !!e && typeof e === "object" && FORMS.includes(e.form) && CID.asCID(e.beef) !== null;
|
|
30
|
+
}
|
|
31
|
+
/** The transaction a BEEF is about: an enveloped form's subject, else the record's last transaction. */
|
|
32
|
+
export function subjectOf(e, r) {
|
|
33
|
+
return e.subject ?? r.txs[r.txs.length - 1];
|
|
34
|
+
}
|
|
35
|
+
const u32 = (n) => { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, n, true); return b; };
|
|
36
|
+
function varint(n) {
|
|
37
|
+
if (n < 0xfd)
|
|
38
|
+
return Uint8Array.of(n);
|
|
39
|
+
if (n <= 0xffff) {
|
|
40
|
+
const b = new Uint8Array(3);
|
|
41
|
+
b[0] = 0xfd;
|
|
42
|
+
new DataView(b.buffer).setUint16(1, n, true);
|
|
43
|
+
return b;
|
|
44
|
+
}
|
|
45
|
+
if (n <= 0xffffffff) {
|
|
46
|
+
const b = new Uint8Array(5);
|
|
47
|
+
b[0] = 0xfe;
|
|
48
|
+
new DataView(b.buffer).setUint32(1, n, true);
|
|
49
|
+
return b;
|
|
50
|
+
}
|
|
51
|
+
const b = new Uint8Array(9);
|
|
52
|
+
b[0] = 0xff;
|
|
53
|
+
new DataView(b.buffer).setBigUint64(1, BigInt(n), true);
|
|
54
|
+
return b;
|
|
55
|
+
}
|
|
56
|
+
/** A bitcoin CID's hash (the txid in internal byte order). */
|
|
57
|
+
const hashOf = (c) => c.multihash.digest;
|
|
58
|
+
const PREFIX = {
|
|
59
|
+
atomic: Uint8Array.of(1, 1, 1, 1),
|
|
60
|
+
outpoint: Uint8Array.of(0x16, 0xa7, 0xbe, 0xef),
|
|
61
|
+
subject: Uint8Array.of(0x57, 0x09, 0xbe, 0xef),
|
|
62
|
+
};
|
|
63
|
+
const concat = (out) => {
|
|
64
|
+
const all = new Uint8Array(out.reduce((s, x) => s + x.length, 0));
|
|
65
|
+
let at = 0;
|
|
66
|
+
for (const x of out) {
|
|
67
|
+
all.set(x, at);
|
|
68
|
+
at += x.length;
|
|
69
|
+
}
|
|
70
|
+
return all;
|
|
71
|
+
};
|
|
72
|
+
/** The BEEF a pointer record stands for (no envelope); `bytes` reads a block (a transaction, a BUMP's bytes). */
|
|
73
|
+
export async function beefOf(r, bytes) {
|
|
74
|
+
const out = [Uint8Array.of(r.version, 0, 0xbe, 0xef), varint(r.bumps.length)];
|
|
75
|
+
for (const b of r.bumps)
|
|
76
|
+
out.push(await bytes(CID.asCID(b.path)));
|
|
77
|
+
out.push(varint(r.txs.length));
|
|
78
|
+
for (const [i, t] of r.txs.entries()) {
|
|
79
|
+
const m = r.marks[i];
|
|
80
|
+
const c = CID.asCID(t);
|
|
81
|
+
if (m === "txid") {
|
|
82
|
+
out.push(Uint8Array.of(2), hashOf(c));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const raw = await bytes(c);
|
|
86
|
+
if (r.version === 1)
|
|
87
|
+
out.push(raw, ...(m === null ? [Uint8Array.of(0)] : [Uint8Array.of(1), varint(m)]));
|
|
88
|
+
else
|
|
89
|
+
out.push(...(m === null ? [Uint8Array.of(0)] : [Uint8Array.of(1), varint(m)]), raw);
|
|
90
|
+
}
|
|
91
|
+
return concat(out);
|
|
92
|
+
}
|
|
93
|
+
/** The wire bytes an envelope stands for: its prefix, then the BEEF its record stands for. */
|
|
94
|
+
export async function wireOf(e, r, bytes) {
|
|
95
|
+
const inner = await beefOf(r, bytes);
|
|
96
|
+
if (e.form === "beef")
|
|
97
|
+
return inner;
|
|
98
|
+
if (e.form === "subject" && r.version !== 2)
|
|
99
|
+
throw new Error("a Subject BEEF (BRC-233) carries a BEEF V2 only");
|
|
100
|
+
if (!e.subject)
|
|
101
|
+
throw new Error(`a ${e.form} envelope names no subject`);
|
|
102
|
+
const out = [PREFIX[e.form], hashOf(CID.asCID(e.subject))];
|
|
103
|
+
if (e.form === "outpoint")
|
|
104
|
+
out.push(u32(e.vout ?? 0));
|
|
105
|
+
out.push(inner);
|
|
106
|
+
return concat(out);
|
|
107
|
+
}
|