@knpkv/relay 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -3
- package/dist/harness.d.ts +8 -4
- package/dist/harness.d.ts.map +1 -1
- package/dist/index.js +128 -17
- package/dist/index.js.map +4 -4
- package/dist/messages.d.ts +44 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/model.d.ts +19 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/wire.d.ts +214 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.js +145 -0
- package/dist/wire.js.map +7 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -50,7 +50,10 @@ const RelayLive = Layer.unwrap(
|
|
|
50
50
|
the run that answers it. `backend` switches the session from its next turn on (`RelayBackendNotConfigured`
|
|
51
51
|
otherwise). `context` is what the person attached, as the product renders it (`{ label, body }`): it is
|
|
52
52
|
placed just before the message for the model, and never appears in the transcript or the Snapshot.
|
|
53
|
-
- `events(ref)`: a `Snapshot` first, with each message's `id
|
|
53
|
+
- `events(ref)`: a `Snapshot` first, with each message's `id`, the `runIds` of a run in flight and the
|
|
54
|
+
requestIds still `queued`. Then:
|
|
55
|
+
- `MessageQueued { requestId }`, then `MessagePlaced { id, requestId, text }` when a run takes it, or
|
|
56
|
+
`MessageWithdrawn { requestId }` when it is cancelled first.
|
|
54
57
|
- `RunStarted { runIds }` and `TextDelta`.
|
|
55
58
|
- `ToolStarted` and `ToolFinished`, each with a server-built, display-safe `summary`. A finished call carries
|
|
56
59
|
its citations, and a write whose product projects one carries a `receipt { summary, providerId, link? }`
|
|
@@ -67,14 +70,31 @@ const RelayLive = Layer.unwrap(
|
|
|
67
70
|
`NotInstalled`, `SignedOut`, `Misconfigured` or `NoCapability` and a one-line fix. Observed, never persisted.
|
|
68
71
|
`SignedOut` comes from an `AuthenticationError` turn failure.
|
|
69
72
|
|
|
73
|
+
## In the browser: `@knpkv/relay/wire`
|
|
74
|
+
|
|
75
|
+
The contract a product's `/…/relay` routes speak, as a separate browser bundle that imports only
|
|
76
|
+
`effect` and `@knpkv/capability`. It holds the harness's event and session schemas, plus:
|
|
77
|
+
|
|
78
|
+
- `RelayStreamFrame`: one `data:` frame of `GET events`, a `RelayEvent`, `Unauthorized` or `StreamFailed`;
|
|
79
|
+
- `RelayMessageRequest`, `RelayCancelRequest` and `RelayDecisionRequest`, the write bodies, and `RelayMessageAccepted`;
|
|
80
|
+
- the typed refusals: `RelayUnavailableError` (503, with the fix), `RelayConflictError` (409, with the
|
|
81
|
+
`RelayConflictState` found) and `RelayBadRequestError` (400).
|
|
82
|
+
|
|
83
|
+
A product narrows `ref` to its own objects and may add a typed `context` to a message. The build fails if
|
|
84
|
+
the bundle reaches anything else, and `pnpm test:pack` checks the packed `dist/wire.js` again.
|
|
85
|
+
`@knpkv/relay-product/client` reads a conversation through it.
|
|
86
|
+
|
|
70
87
|
## Security boundaries
|
|
71
88
|
|
|
72
89
|
- The session store holds conversation content. Its directory is created `0700` and the database `0600`;
|
|
73
90
|
a store directory, database or SQLite sidecar (`-wal`, `-shm`) that is a symbolic link, even a dangling one, fails with `RelayStoreLinked` before anything is
|
|
74
91
|
re-permissioned or written;
|
|
75
92
|
it is never logged, never sent to telemetry, and never served on unauthenticated routes.
|
|
76
|
-
- One process owns a store:
|
|
77
|
-
with `RelayStoreLocked
|
|
93
|
+
- One process owns a store: it holds an exclusive SQLite lock on `<store>.lock` (owner-only), so a second
|
|
94
|
+
owner fails with `RelayStoreLocked`. Closing the harness frees the store at once, so the same process can
|
|
95
|
+
open it again; the OS releases the lock if the owner dies. The lock is the runtime's own SQLite
|
|
96
|
+
(`node:sqlite` on Node, `bun:sqlite` on Bun), loaded when a store opens and used only through `exec`,
|
|
97
|
+
because libsql keeps a closed connection, and its locks, until its statements are garbage-collected.
|
|
78
98
|
- Provider credentials stay with the user's CLI. Relay only spawns `claude`/`codex` with all of their tools
|
|
79
99
|
withheld.
|
|
80
100
|
|
package/dist/harness.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context, Crypto, Effect, FileSystem, Layer, Path, Schema, Stream } from "effect";
|
|
1
|
+
import { Context, Crypto, Effect, FileSystem, Layer, Path, Schema, Scope, Stream } from "effect";
|
|
2
2
|
import type { LanguageModel } from "effect/ai";
|
|
3
3
|
import { RelayBackendId } from "./model.js";
|
|
4
4
|
import type { BackendStatus, ObjectRef, RelayEvent, SessionInfo, SessionTool } from "./model.js";
|
|
@@ -120,9 +120,13 @@ export interface RelayHarnessService {
|
|
|
120
120
|
declare const RelayHarness_base: Context.ServiceClass<RelayHarness, "@knpkv/relay/RelayHarness", RelayHarnessService>;
|
|
121
121
|
export declare class RelayHarness extends RelayHarness_base {
|
|
122
122
|
}
|
|
123
|
-
/**
|
|
124
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Build the harness for one product. One attempt's resources (the store lock, the store, the harness) live in
|
|
125
|
+
* their own scope: an attempt that fails releases them at once, so a retry in the same scope can take the
|
|
126
|
+
* store; one that succeeds keeps them until the caller's scope closes.
|
|
127
|
+
*/
|
|
128
|
+
export declare const make: <Requirements>(options: RelayHarnessOptions<Requirements>) => Effect.Effect<RelayHarnessService, RelayStoreFailed | RelayStoreLinked | RelayStoreLocked, Crypto.Crypto | FileSystem.FileSystem | Path.Path | Scope.Scope | Exclude<Requirements, Scope.Scope>>;
|
|
125
129
|
/** Build the harness as a scoped layer: the store closes, and the lock is released, with the scope. */
|
|
126
|
-
export declare const layer: <Requirements>(options: RelayHarnessOptions<Requirements>) => Layer.Layer<RelayHarness, RelayStoreFailed | RelayStoreLinked | RelayStoreLocked, Crypto.Crypto | FileSystem.FileSystem | Path.Path | Exclude<Requirements,
|
|
130
|
+
export declare const layer: <Requirements>(options: RelayHarnessOptions<Requirements>) => Layer.Layer<RelayHarness, RelayStoreFailed | RelayStoreLinked | RelayStoreLocked, Crypto.Crypto | FileSystem.FileSystem | Path.Path | Exclude<Exclude<Requirements, Scope.Scope>, Scope.Scope>>;
|
|
127
131
|
export {};
|
|
128
132
|
//# sourceMappingURL=harness.d.ts.map
|
package/dist/harness.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -4757,10 +4757,10 @@ async function finalResult(runtime, call, result, reported, context) {
|
|
|
4757
4757
|
harness.push(truncated(bounded, reported.limits.retain));
|
|
4758
4758
|
return { ...final, content: bounded.content, diagnostics: [...final.diagnostics ?? [], ...harness] };
|
|
4759
4759
|
}
|
|
4760
|
-
async function settle(runtime, call, ending,
|
|
4760
|
+
async function settle(runtime, call, ending, build2, context) {
|
|
4761
4761
|
await runtime.commit(async (tx) => {
|
|
4762
4762
|
const slot = toolSlot(await tx.doc(LiveDoc, runtime.conversationId), runtime.taskId);
|
|
4763
|
-
const result =
|
|
4763
|
+
const result = build2(slot);
|
|
4764
4764
|
const entry = await appendToolResult(tx, runtime.conversationId, call, result, runtime.now());
|
|
4765
4765
|
if (slot !== void 0)
|
|
4766
4766
|
finishSlot(slot, entry.id);
|
|
@@ -9257,11 +9257,11 @@ var HarnessImpl = class extends SessionImpl {
|
|
|
9257
9257
|
}
|
|
9258
9258
|
/** Build a conversation's environment from its current `cwd`; `undefined` without an `env` option. */
|
|
9259
9259
|
async buildEnv(id, context) {
|
|
9260
|
-
const
|
|
9261
|
-
if (
|
|
9260
|
+
const build2 = this.#options.env;
|
|
9261
|
+
if (build2 === void 0)
|
|
9262
9262
|
return void 0;
|
|
9263
9263
|
const cwd = (await this.snapshot(AgentDoc, id, context))?.cwd;
|
|
9264
|
-
return
|
|
9264
|
+
return build2({ conversationId: id, ...cwd === void 0 ? {} : { cwd }, read: this }, context);
|
|
9265
9265
|
}
|
|
9266
9266
|
/** Reconcile surviving `running` tasks to `pending`; part of open. */
|
|
9267
9267
|
openTasks(context) {
|
|
@@ -10117,8 +10117,9 @@ var SqliteStorage = class _SqliteStorage {
|
|
|
10117
10117
|
// dist/harness.js
|
|
10118
10118
|
import * as Capability2 from "@knpkv/capability";
|
|
10119
10119
|
import { createClient } from "@libsql/client";
|
|
10120
|
-
import { Clock, Context, Crypto, Deferred, Effect as Effect3, Exit, FileSystem, Layer, Path, Predicate as
|
|
10120
|
+
import { Clock, Context, Crypto, Deferred, Effect as Effect3, Exit, FileSystem, Layer, Path, Predicate as Predicate4, PubSub, Queue, Ref, Schema as Schema5, Scope, Semaphore, Stream } from "effect";
|
|
10121
10121
|
import { AiError } from "effect/ai";
|
|
10122
|
+
import { createRequire } from "node:module";
|
|
10122
10123
|
|
|
10123
10124
|
// dist/libsqlDatabase.js
|
|
10124
10125
|
import { Predicate } from "effect";
|
|
@@ -10174,6 +10175,48 @@ var libsqlDatabase = (client) => {
|
|
|
10174
10175
|
};
|
|
10175
10176
|
};
|
|
10176
10177
|
|
|
10178
|
+
// dist/messages.js
|
|
10179
|
+
import { Predicate as Predicate2 } from "effect";
|
|
10180
|
+
var entryText = (messages) => (messages ?? []).flatMap((message) => message.role === "user" || message.role === "assistant" ? Predicate2.isString(message.content) ? [message.content] : message.content.flatMap((block) => block.type === "text" && block.text !== void 0 ? [block.text] : []) : []).join("\n");
|
|
10181
|
+
var makeMessageTracker = () => {
|
|
10182
|
+
const texts = /* @__PURE__ */ new Map();
|
|
10183
|
+
const requests = /* @__PURE__ */ new Map();
|
|
10184
|
+
const announced = /* @__PURE__ */ new Set();
|
|
10185
|
+
const placed = (entry) => {
|
|
10186
|
+
const text = texts.get(entry);
|
|
10187
|
+
const requestId = requests.get(entry);
|
|
10188
|
+
if (text === void 0 || requestId === void 0 || announced.has(entry))
|
|
10189
|
+
return [];
|
|
10190
|
+
announced.add(entry);
|
|
10191
|
+
texts.delete(entry);
|
|
10192
|
+
requests.delete(entry);
|
|
10193
|
+
return [{ _tag: "MessagePlaced", id: entry, requestId, text }];
|
|
10194
|
+
};
|
|
10195
|
+
return {
|
|
10196
|
+
/** A committed entry; only a person's message can place one. */
|
|
10197
|
+
entry: (entry) => {
|
|
10198
|
+
if (entry.kind !== "pi.user" || announced.has(String(entry.id)))
|
|
10199
|
+
return [];
|
|
10200
|
+
texts.set(String(entry.id), entryText(entry.model));
|
|
10201
|
+
return placed(String(entry.id));
|
|
10202
|
+
},
|
|
10203
|
+
/** A submission record; Relay submits every input with the dock's requestId, its id is the fallback. */
|
|
10204
|
+
record: (record) => {
|
|
10205
|
+
if (record.type !== "input")
|
|
10206
|
+
return [];
|
|
10207
|
+
const requestId = record.requestId ?? String(record.id);
|
|
10208
|
+
if (record.entry === void 0) {
|
|
10209
|
+
return [{ _tag: record.status === "queued" ? "MessageQueued" : "MessageWithdrawn", requestId }];
|
|
10210
|
+
}
|
|
10211
|
+
const entry = String(record.entry);
|
|
10212
|
+
if (announced.has(entry))
|
|
10213
|
+
return [];
|
|
10214
|
+
requests.set(entry, requestId);
|
|
10215
|
+
return placed(entry);
|
|
10216
|
+
}
|
|
10217
|
+
};
|
|
10218
|
+
};
|
|
10219
|
+
|
|
10177
10220
|
// dist/model.js
|
|
10178
10221
|
import * as Capability from "@knpkv/capability";
|
|
10179
10222
|
import { Schema } from "effect";
|
|
@@ -10228,8 +10271,19 @@ var RelayEvent = Schema.TaggedUnion({
|
|
|
10228
10271
|
...eventFields,
|
|
10229
10272
|
messages: Schema.Array(Schema.Struct({ id: Name, role: Schema.Literals(["user", "relay"]), text: Schema.String })),
|
|
10230
10273
|
/** The run in flight, empty when idle. */
|
|
10231
|
-
runIds: RunIds
|
|
10274
|
+
runIds: RunIds,
|
|
10275
|
+
/** Messages accepted but not yet in the transcript, oldest first: they wait for the run in flight. */
|
|
10276
|
+
queued: RunIds
|
|
10232
10277
|
},
|
|
10278
|
+
/**
|
|
10279
|
+
* A message the server accepted that waits for the run in flight. It joins the transcript with its own
|
|
10280
|
+
* `MessagePlaced`, or leaves with `MessageWithdrawn`.
|
|
10281
|
+
*/
|
|
10282
|
+
MessageQueued: { ...eventFields, requestId: Name },
|
|
10283
|
+
/** A message the server placed in the transcript; `id` is its transcript id, as a later Snapshot names it. */
|
|
10284
|
+
MessagePlaced: { ...eventFields, id: Name, requestId: Name, text: Schema.String },
|
|
10285
|
+
/** A queued message the person withdrew before any run took it. */
|
|
10286
|
+
MessageWithdrawn: { ...eventFields, requestId: Name },
|
|
10233
10287
|
RunStarted: { ...eventFields, runIds: RunIds },
|
|
10234
10288
|
TextDelta: { ...eventFields, text: Schema.String },
|
|
10235
10289
|
/** `summary` is a server-built, display-safe line; `input` is the call's typed arguments. */
|
|
@@ -11309,11 +11363,11 @@ function createProvider(input) {
|
|
|
11309
11363
|
}
|
|
11310
11364
|
|
|
11311
11365
|
// dist/piProvider.js
|
|
11312
|
-
import { Predicate as
|
|
11366
|
+
import { Predicate as Predicate3, Schema as Schema2 } from "effect";
|
|
11313
11367
|
var API = "relay-cli-turn";
|
|
11314
11368
|
var decodeJson = Schema2.decodeUnknownOption(Schema2.Json);
|
|
11315
11369
|
var MODEL_ID = "default";
|
|
11316
|
-
var textOf = (content) =>
|
|
11370
|
+
var textOf = (content) => Predicate3.isString(content) ? content : content.flatMap((block) => block.type === "text" && block.text !== void 0 ? [block.text] : []).join("\n");
|
|
11317
11371
|
var turnRequestOf = (messages) => {
|
|
11318
11372
|
const instructions = [];
|
|
11319
11373
|
const tools = /* @__PURE__ */ new Map();
|
|
@@ -11636,7 +11690,48 @@ var promise = (operation, run2) => Effect3.tryPromise({
|
|
|
11636
11690
|
try: run2,
|
|
11637
11691
|
catch: (cause) => new RelayStoreFailed({ operation, message: `${operation} failed: ${String(cause)}` })
|
|
11638
11692
|
});
|
|
11639
|
-
var notALink = (error) => error.reason._tag === "NotFound" || error.reason._tag !== "BadArgument" &&
|
|
11693
|
+
var notALink = (error) => error.reason._tag === "NotFound" || error.reason._tag !== "BadArgument" && Predicate4.hasProperty(error.reason.cause, "code") && error.reason.cause.code === "EINVAL";
|
|
11694
|
+
var lockDriver = Effect3.tryPromise({
|
|
11695
|
+
try: async () => {
|
|
11696
|
+
try {
|
|
11697
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
11698
|
+
return (path) => new DatabaseSync(path);
|
|
11699
|
+
} catch (nodeFailure) {
|
|
11700
|
+
try {
|
|
11701
|
+
const bun = createRequire(import.meta.url)("bun:sqlite");
|
|
11702
|
+
return (path) => new bun.Database(path);
|
|
11703
|
+
} catch (bunFailure) {
|
|
11704
|
+
throw new AggregateError([nodeFailure, bunFailure], `neither node:sqlite (${String(nodeFailure)}) nor bun:sqlite (${String(bunFailure)}) could load`, { cause: bunFailure });
|
|
11705
|
+
}
|
|
11706
|
+
}
|
|
11707
|
+
},
|
|
11708
|
+
catch: (cause) => new RelayStoreFailed({
|
|
11709
|
+
operation: "load SQLite for the Relay store lock",
|
|
11710
|
+
message: `load SQLite for the Relay store lock failed: ${String(cause)}`
|
|
11711
|
+
})
|
|
11712
|
+
});
|
|
11713
|
+
var isBusy = (cause) => Predicate4.hasProperty(cause, "errcode") && cause.errcode === 5 || Predicate4.hasProperty(cause, "errno") && cause.errno === 5 || Predicate4.hasProperty(cause, "code") && cause.code === "SQLITE_BUSY";
|
|
11714
|
+
var acquireStoreLock = (path) => Effect3.acquireRelease(Effect3.flatMap(lockDriver, (open) => Effect3.try({
|
|
11715
|
+
try: () => {
|
|
11716
|
+
const lock = open(path);
|
|
11717
|
+
try {
|
|
11718
|
+
lock.exec("PRAGMA locking_mode = EXCLUSIVE");
|
|
11719
|
+
lock.exec("BEGIN IMMEDIATE");
|
|
11720
|
+
lock.exec("COMMIT");
|
|
11721
|
+
return lock;
|
|
11722
|
+
} catch (failure) {
|
|
11723
|
+
lock.close();
|
|
11724
|
+
throw failure;
|
|
11725
|
+
}
|
|
11726
|
+
},
|
|
11727
|
+
catch: (cause) => isBusy(cause) ? new RelayStoreLocked({
|
|
11728
|
+
path,
|
|
11729
|
+
message: `Another process owns the Relay store at ${path.replace(/\.lock$/u, "")}. Stop it, then retry.`
|
|
11730
|
+
}) : new RelayStoreFailed({
|
|
11731
|
+
operation: "lock the Relay store",
|
|
11732
|
+
message: `lock the Relay store failed: ${String(cause)}`
|
|
11733
|
+
})
|
|
11734
|
+
})), (lock) => Effect3.sync(() => lock.close()));
|
|
11640
11735
|
var openStore = (path) => Effect3.acquireRelease(Effect3.gen(function* () {
|
|
11641
11736
|
const fs = yield* FileSystem.FileSystem;
|
|
11642
11737
|
const paths = yield* Path.Path;
|
|
@@ -11654,17 +11749,17 @@ var openStore = (path) => Effect3.acquireRelease(Effect3.gen(function* () {
|
|
|
11654
11749
|
}));
|
|
11655
11750
|
yield* refuseLink(directory, "store directory");
|
|
11656
11751
|
yield* fs.makeDirectory(directory, { recursive: true, mode: 448 }).pipe(ownerOnly("create the Relay store directory"));
|
|
11657
|
-
yield* Effect3.forEach([path, `${path}-wal`, `${path}-shm`], (file) => refuseLink(file, "store"), {
|
|
11752
|
+
yield* Effect3.forEach([path, `${path}-wal`, `${path}-shm`, `${path}.lock`], (file) => refuseLink(file, "store"), {
|
|
11658
11753
|
discard: true
|
|
11659
11754
|
});
|
|
11660
11755
|
yield* fs.chmod(directory, 448).pipe(ownerOnly("restrict the Relay store directory"));
|
|
11756
|
+
yield* acquireStoreLock(`${path}.lock`);
|
|
11757
|
+
yield* fs.chmod(`${path}.lock`, 384).pipe(ownerOnly("restrict the Relay store lock"));
|
|
11661
11758
|
const client = createClient({ url: `file:${path}` });
|
|
11662
11759
|
const database = libsqlDatabase(client);
|
|
11663
11760
|
yield* promise("open the Relay store", async () => {
|
|
11664
|
-
await database.exec("PRAGMA locking_mode = EXCLUSIVE");
|
|
11665
11761
|
await database.exec("PRAGMA journal_mode = WAL");
|
|
11666
11762
|
await database.exec("PRAGMA synchronous = NORMAL");
|
|
11667
|
-
await database.exec("BEGIN IMMEDIATE; COMMIT");
|
|
11668
11763
|
}).pipe(Effect3.andThen(fs.chmod(path, 384).pipe(ownerOnly("restrict the Relay store"))), Effect3.catchTag("RelayStoreFailed", (failure) => /SQLITE_BUSY|database is locked/u.test(failure.message) ? Effect3.fail(new RelayStoreLocked({
|
|
11669
11764
|
path,
|
|
11670
11765
|
message: `Another process owns the Relay store at ${path}. Stop it, then retry.`
|
|
@@ -11672,6 +11767,11 @@ var openStore = (path) => Effect3.acquireRelease(Effect3.gen(function* () {
|
|
|
11672
11767
|
return database;
|
|
11673
11768
|
}), (database) => Effect3.promise(() => database.close()));
|
|
11674
11769
|
var make = Effect3.fn("RelayHarness.make")(function* (options) {
|
|
11770
|
+
const owner = yield* Scope.Scope;
|
|
11771
|
+
const attempt = yield* Scope.fork(owner, "sequential");
|
|
11772
|
+
return yield* build(options).pipe(Scope.provide(attempt), Effect3.onExit((exit) => Exit.isFailure(exit) ? Scope.close(attempt, exit) : Effect3.void));
|
|
11773
|
+
});
|
|
11774
|
+
var build = Effect3.fn("RelayHarness.build")(function* (options) {
|
|
11675
11775
|
const services = yield* Effect3.context();
|
|
11676
11776
|
const runEffect = (effect, signal) => Effect3.runPromiseExitWith(services)(effect, signal === void 0 ? void 0 : { signal });
|
|
11677
11777
|
const runPromise = Effect3.runPromiseWith(services);
|
|
@@ -11861,7 +11961,6 @@ ${item.body}`,
|
|
|
11861
11961
|
});
|
|
11862
11962
|
});
|
|
11863
11963
|
var layer = (options) => Layer.effect(RelayHarness, make(options));
|
|
11864
|
-
var entryText = (messages) => (messages ?? []).flatMap((message) => message.role === "user" || message.role === "assistant" ? Predicate3.isString(message.content) ? [message.content] : message.content.flatMap((block) => block.type === "text" ? [block.text] : []) : []).join("\n");
|
|
11865
11964
|
var decodeDetails = Schema5.decodeUnknownOption(Schema5.Struct({ cites: Schema5.Array(Capability2.ObjectRef), receipt: Schema5.optionalKey(WriteReceipt) }));
|
|
11866
11965
|
var sessionEvents = (harness, conversation, confirmations, pendingFor, display) => {
|
|
11867
11966
|
const summarize = (capability, args) => display(capability)?.summarize(args) ?? capability;
|
|
@@ -11905,7 +12004,9 @@ var sessionEvents = (harness, conversation, confirmations, pendingFor, display)
|
|
|
11905
12004
|
}]);
|
|
11906
12005
|
return [...partial, ...running];
|
|
11907
12006
|
};
|
|
11908
|
-
const
|
|
12007
|
+
const messages = makeMessageTracker();
|
|
12008
|
+
const fromMessages = (events) => events.map((event) => ({ ...event, session }));
|
|
12009
|
+
const toRelay = (event, runIds, queued2 = []) => {
|
|
11909
12010
|
switch (event.type) {
|
|
11910
12011
|
case "snapshot":
|
|
11911
12012
|
return [
|
|
@@ -11913,6 +12014,7 @@ var sessionEvents = (harness, conversation, confirmations, pendingFor, display)
|
|
|
11913
12014
|
_tag: "Snapshot",
|
|
11914
12015
|
session,
|
|
11915
12016
|
runIds,
|
|
12017
|
+
queued: queued2,
|
|
11916
12018
|
messages: event.entries.flatMap((entry) => entry.kind === "pi.user" ? [{ id: String(entry.id), role: "user", text: entryText(entry.model) }] : entry.kind === "pi.assistant" ? [{ id: String(entry.id), role: "relay", text: entryText(entry.model) }] : [])
|
|
11917
12019
|
},
|
|
11918
12020
|
...inFlight(event),
|
|
@@ -11921,6 +12023,11 @@ var sessionEvents = (harness, conversation, confirmations, pendingFor, display)
|
|
|
11921
12023
|
case "message_start":
|
|
11922
12024
|
sent.clear();
|
|
11923
12025
|
return event.message.role === "assistant" ? event.message.content.flatMap((block, index) => block.type === "text" ? textFrom(index, block.text) : []) : [];
|
|
12026
|
+
case "message_end":
|
|
12027
|
+
case "entry_appended":
|
|
12028
|
+
return fromMessages(messages.entry(event.entry));
|
|
12029
|
+
case "submission":
|
|
12030
|
+
return fromMessages(messages.record(event.record));
|
|
11924
12031
|
case "message_update":
|
|
11925
12032
|
return event.changes.flatMap((change) => {
|
|
11926
12033
|
if (change.type === "text_delta") {
|
|
@@ -11979,6 +12086,10 @@ var sessionEvents = (harness, conversation, confirmations, pendingFor, display)
|
|
|
11979
12086
|
const runIdsOf = (inputs, read2) => inputs.map((id, index) => read2[index]?.requestId ?? String(id));
|
|
11980
12087
|
let active = [];
|
|
11981
12088
|
const track2 = (inputs) => (inputs.length === 0 ? Effect3.succeed([]) : promise("read the run in flight", async () => runIdsOf(inputs, await records(inputs)))).pipe(Effect3.tap((runIds) => Effect3.sync(() => active = runIds)));
|
|
12089
|
+
const queuedOf = (inbox) => {
|
|
12090
|
+
const inputs = inbox.map(({ id }) => id);
|
|
12091
|
+
return inputs.length === 0 ? Effect3.succeed([]) : promise("read the queued messages", async () => runIdsOf(inputs, await records(inputs)));
|
|
12092
|
+
};
|
|
11982
12093
|
const runEnded = (inputs) => promise("read how the run ended", async () => {
|
|
11983
12094
|
const read2 = await records(inputs);
|
|
11984
12095
|
const runIds = runIdsOf(inputs, read2);
|
|
@@ -11991,13 +12102,13 @@ var sessionEvents = (harness, conversation, confirmations, pendingFor, display)
|
|
|
11991
12102
|
session,
|
|
11992
12103
|
runIds,
|
|
11993
12104
|
// `reason` is Pi's code (`model_error`); `detail` carries the backend's own message when there is one.
|
|
11994
|
-
cause:
|
|
12105
|
+
cause: Predicate4.isString(unanswered.detail) ? unanswered.detail : unanswered.reason,
|
|
11995
12106
|
fix: "Check the backend in Relay setup (installed and signed in), then send the message again."
|
|
11996
12107
|
};
|
|
11997
12108
|
});
|
|
11998
12109
|
const gate = Stream.fromPubSub(confirmations).pipe(Stream.filter(({ conversationId }) => conversationId === session), Stream.map(({ event }) => event));
|
|
11999
12110
|
let seq = 0;
|
|
12000
|
-
const relayed = Stream.flatMap(fromPi, (event) => event.type === "run_end" ? Stream.fromEffect(runEnded(event.inputs).pipe(Effect3.tap(() => Effect3.sync(() => active = [])))) : event.type === "run_start" ? Stream.fromEffect(track2(event.inputs)).pipe(Stream.map((runIds) => ({ _tag: "RunStarted", session, runIds }))) : event.type === "snapshot" ? Stream.fromEffect(track2(event.run?.inputs ?? [])).pipe(Stream.flatMap((runIds) => Stream.fromIterable(toRelay(event, runIds)))) : Stream.fromIterable(toRelay(event, active)));
|
|
12111
|
+
const relayed = Stream.flatMap(fromPi, (event) => event.type === "run_end" ? Stream.fromEffect(runEnded(event.inputs).pipe(Effect3.tap(() => Effect3.sync(() => active = [])))) : event.type === "run_start" ? Stream.fromEffect(track2(event.inputs)).pipe(Stream.map((runIds) => ({ _tag: "RunStarted", session, runIds }))) : event.type === "snapshot" ? Stream.fromEffect(Effect3.all([track2(event.run?.inputs ?? []), queuedOf(event.inbox)])).pipe(Stream.flatMap(([runIds, queued2]) => Stream.fromIterable(toRelay(event, runIds, queued2)))) : Stream.fromIterable(toRelay(event, active)));
|
|
12001
12112
|
return Stream.merge(relayed, gate).pipe(Stream.map((event) => ({ ...event, seq: seq++ })));
|
|
12002
12113
|
};
|
|
12003
12114
|
|