@dxos/assistant 0.14.0 → 0.16.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/dist/lib/ExecutionGraph.mjs.map +1 -1
- package/dist/lib/chunk-Agent.mjs +202 -0
- package/dist/lib/chunk-Agent.mjs.map +1 -0
- package/dist/lib/chunk-AiContext.mjs +462 -0
- package/dist/lib/chunk-AiContext.mjs.map +1 -0
- package/dist/lib/chunk-AiRequest.mjs.map +1 -1
- package/dist/lib/chunk-AiSession.mjs +2 -2
- package/dist/lib/chunk-AiSession.mjs.map +1 -1
- package/dist/lib/chunk-Alarm.mjs +37 -0
- package/dist/lib/chunk-Alarm.mjs.map +1 -0
- package/dist/lib/chunk-Chat.mjs +258 -0
- package/dist/lib/chunk-Chat.mjs.map +1 -0
- package/dist/lib/chunk-Harness.mjs +4 -4
- package/dist/lib/chunk-Harness.mjs.map +1 -1
- package/dist/lib/chunk-SessionLink.mjs +20 -0
- package/dist/lib/chunk-SessionLink.mjs.map +1 -0
- package/dist/lib/chunk-SkillHooks.mjs +47 -0
- package/dist/lib/chunk-SkillHooks.mjs.map +1 -0
- package/dist/lib/extraction.mjs.map +1 -1
- package/dist/lib/index.mjs +6 -6
- package/dist/lib/index.mjs.map +1 -1
- package/dist/lib/request/AiRequest.mjs +1 -2
- package/dist/lib/session/AiContext.mjs +1 -462
- package/dist/lib/session/AiSession.mjs +1 -2
- package/dist/lib/session/Alarm.mjs +1 -37
- package/dist/lib/session/Harness.mjs +1 -2
- package/dist/lib/session/SessionLink.mjs +1 -20
- package/dist/lib/session/SkillHooks.mjs +1 -47
- package/dist/lib/types/Agent.mjs +1 -202
- package/dist/lib/types/Chat.mjs +1 -258
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +23 -23
- package/src/request/AiRequest.test.ts +1 -1
- package/src/types/Agent.ts +1 -1
- package/dist/lib/session/AiContext.mjs.map +0 -1
- package/dist/lib/session/Alarm.mjs.map +0 -1
- package/dist/lib/session/SessionLink.mjs.map +0 -1
- package/dist/lib/session/SkillHooks.mjs.map +0 -1
- package/dist/lib/types/Agent.mjs.map +0 -1
- package/dist/lib/types/Chat.mjs.map +0 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { t as __exportAll } from "./chunk-rolldown-runtime.mjs";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import { DXN, Obj, Type } from "@dxos/echo";
|
|
4
|
+
//#region src/session/Alarm.ts
|
|
5
|
+
var Alarm_exports = /* @__PURE__ */ __exportAll({
|
|
6
|
+
Alarm: () => Alarm,
|
|
7
|
+
MAX_SELF_WAKES: () => 10,
|
|
8
|
+
make: () => make
|
|
9
|
+
});
|
|
10
|
+
/**
|
|
11
|
+
* Feed-level record scheduling a future self-wake of the agent.
|
|
12
|
+
* Managed with regular feed CRUD: append schedules, `Feed.remove` cancels a pending alarm. When it
|
|
13
|
+
* fires, the agent appends the wake-up `Message` carrying `SessionStore.AckAnnotation` naming this
|
|
14
|
+
* record — the ack filters it out of the pending set; the record itself stays in the feed.
|
|
15
|
+
*/
|
|
16
|
+
var Alarm = class extends Type.makeObject(DXN.make("org.dxos.type.alarm", "0.1.0"))(Schema.Struct({
|
|
17
|
+
/** Epoch milliseconds at which the agent should wake. */
|
|
18
|
+
wakeAt: Schema.Number,
|
|
19
|
+
/** Reminder surfaced to the agent when the alarm fires. */
|
|
20
|
+
message: Schema.optional(Schema.String),
|
|
21
|
+
/** ISO timestamp when the alarm was scheduled. */
|
|
22
|
+
created: Schema.String
|
|
23
|
+
})) {};
|
|
24
|
+
/**
|
|
25
|
+
* Most alarms that may wake the agent in a row without a user prompt between them; each wake is a full turn over the
|
|
26
|
+
* whole context, so an agent polling on alarms would otherwise spend without bound.
|
|
27
|
+
*/
|
|
28
|
+
var MAX_SELF_WAKES = 10;
|
|
29
|
+
var make = ({ wakeAt, message, created }) => Obj.make(Alarm, {
|
|
30
|
+
wakeAt,
|
|
31
|
+
message,
|
|
32
|
+
created: created ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
33
|
+
});
|
|
34
|
+
//#endregion
|
|
35
|
+
export { make as i, Alarm_exports as n, MAX_SELF_WAKES as r, Alarm as t };
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=chunk-Alarm.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-Alarm.mjs","names":[],"sources":["../../src/session/Alarm.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\n// @import-as-namespace\n\nimport * as Schema from 'effect/Schema';\n\nimport { DXN, Obj, Type } from '@dxos/echo';\n\n/**\n * Feed-level record scheduling a future self-wake of the agent.\n * Managed with regular feed CRUD: append schedules, `Feed.remove` cancels a pending alarm. When it\n * fires, the agent appends the wake-up `Message` carrying `SessionStore.AckAnnotation` naming this\n * record — the ack filters it out of the pending set; the record itself stays in the feed.\n */\nexport class Alarm extends Type.makeObject<Alarm>(DXN.make('org.dxos.type.alarm', '0.1.0'))(\n Schema.Struct({\n /** Epoch milliseconds at which the agent should wake. */\n wakeAt: Schema.Number,\n /** Reminder surfaced to the agent when the alarm fires. */\n message: Schema.optional(Schema.String),\n /** ISO timestamp when the alarm was scheduled. */\n created: Schema.String,\n }),\n) {}\n\n/**\n * Most alarms that may wake the agent in a row without a user prompt between them; each wake is a full turn over the\n * whole context, so an agent polling on alarms would otherwise spend without bound.\n */\nexport const MAX_SELF_WAKES = 10;\n\nexport type MakeProps = {\n wakeAt: number;\n message?: string;\n created?: string;\n};\n\nexport const make = ({ wakeAt, message, created }: MakeProps): Alarm =>\n Obj.make(Alarm, {\n wakeAt,\n message,\n created: created ?? new Date().toISOString(),\n });\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,IAAa,QAAb,cAA2B,KAAK,WAAkB,IAAI,KAAK,uBAAuB,OAAO,CAAC,CAAC,CACzF,OAAO,OAAO;;CAEZ,QAAQ,OAAO;;CAEf,SAAS,OAAO,SAAS,OAAO,MAAM;;CAEtC,SAAS,OAAO;AAClB,CAAC,CACH,CAAC,CAAC,CAAC;;;;;AAMH,IAAa,iBAAiB;AAQ9B,IAAa,QAAQ,EAAE,QAAQ,SAAS,cACtC,IAAI,KAAK,OAAO;CACd;CACA;CACA,SAAS,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;AAC7C,CAAC"}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { t as __exportAll } from "./chunk-rolldown-runtime.mjs";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Option from "effect/Option";
|
|
4
|
+
import * as Schema from "effect/Schema";
|
|
5
|
+
import { SessionConfig } from "@dxos/ai";
|
|
6
|
+
import { Annotation, DXN, Database, Feed, Filter, Obj, Ref, Type } from "@dxos/echo";
|
|
7
|
+
import { log } from "@dxos/log";
|
|
8
|
+
import { Task } from "@dxos/types";
|
|
9
|
+
import * as Cause from "effect/Cause";
|
|
10
|
+
import * as Instructions from "@dxos/compute/Instructions";
|
|
11
|
+
import * as Project from "@dxos/compute/Project";
|
|
12
|
+
import { ArchivableAnnotation } from "@dxos/schema";
|
|
13
|
+
//#region src/types/Chat.ts
|
|
14
|
+
var Chat_exports = /* @__PURE__ */ __exportAll({
|
|
15
|
+
Chat: () => Chat,
|
|
16
|
+
CompanionChatAnnotation: () => CompanionChatAnnotation,
|
|
17
|
+
addTask: () => addTask,
|
|
18
|
+
assignTasks: () => assignTasks,
|
|
19
|
+
deleteTask: () => deleteTask,
|
|
20
|
+
feedEntityId: () => feedEntityId,
|
|
21
|
+
fields: () => fields,
|
|
22
|
+
formatChecklist: () => formatChecklist,
|
|
23
|
+
isOpenTask: () => isOpenTask,
|
|
24
|
+
linkCompanion: () => linkCompanion,
|
|
25
|
+
loadForFeed: () => loadForFeed,
|
|
26
|
+
loadTasks: () => loadTasks,
|
|
27
|
+
make: () => make,
|
|
28
|
+
peekProject: () => peekProject,
|
|
29
|
+
resolveTasks: () => resolveTasks,
|
|
30
|
+
seedSession: () => seedSession,
|
|
31
|
+
unassignTasks: () => unassignTasks
|
|
32
|
+
});
|
|
33
|
+
var __dxlog_file = "/__w/dxos/dxos/packages/core/compute/assistant/src/types/Chat.ts";
|
|
34
|
+
/**
|
|
35
|
+
* AI chat session.
|
|
36
|
+
*/
|
|
37
|
+
var Chat = class extends Type.makeObject(DXN.make("org.dxos.type.assistant.chat", "0.2.0"))(Schema.Struct({
|
|
38
|
+
name: Schema.String.pipe(Schema.optional),
|
|
39
|
+
viewType: Schema.String.pipe(Schema.optional),
|
|
40
|
+
/**
|
|
41
|
+
* Runs this conversation's agent on the edge rather than locally, mirroring a trigger's own
|
|
42
|
+
* `remote` flag. When unset, the agent runs locally on the client.
|
|
43
|
+
*/
|
|
44
|
+
remote: Schema.Boolean.pipe(Schema.annotate({ title: "Remote" }), Schema.optional),
|
|
45
|
+
/** Message feed, owned by the chat so `SetParent` cascades it. */
|
|
46
|
+
feed: Ref.Ref(Feed.Feed).pipe(Annotation.SetParent.set(), Annotation.FormInputAnnotation.set(false)),
|
|
47
|
+
/**
|
|
48
|
+
* Instructions steering this conversation, rendered into the system prompt at request time.
|
|
49
|
+
* Held by reference (never copied), so a project's chats follow edits to its instructions.
|
|
50
|
+
*/
|
|
51
|
+
instructions: Schema.optional(Ref.Ref(Instructions.Instructions).pipe(Annotation.FormInputAnnotation.set(false))),
|
|
52
|
+
/**
|
|
53
|
+
* How this conversation runs (its model), chosen in the chat rather than globally so it survives
|
|
54
|
+
* a remount and travels with the chat. Unset fields fall back to the project's, then the agent's.
|
|
55
|
+
*/
|
|
56
|
+
session: Schema.optional(SessionConfig.SessionConfig.pipe(Annotation.FormInputAnnotation.set(false))),
|
|
57
|
+
/**
|
|
58
|
+
* The working checklist, flat and ordered. Deliberately NOT an owning (`SetParent`) field: a
|
|
59
|
+
* chat may work on a task that belongs somewhere else — a project's task set delegates one here
|
|
60
|
+
* — and an owning field re-parents every resolved member on each update of the chat, which
|
|
61
|
+
* would silently move that task out of the set that owns it. Tasks the chat itself creates are
|
|
62
|
+
* parented to it explicitly; see {@link addTask}.
|
|
63
|
+
*/
|
|
64
|
+
tasks: Schema.Array(Ref.Ref(Task.Task)).pipe(Annotation.FormInputAnnotation.set(false))
|
|
65
|
+
}).pipe(Annotation.LabelAnnotation.set(["name"]), Annotation.IconAnnotation.set({
|
|
66
|
+
icon: "ph--sparkle--regular",
|
|
67
|
+
hue: "amber"
|
|
68
|
+
}), Annotation.UserType.set(), ArchivableAnnotation.set(true))) {};
|
|
69
|
+
/** Module-level alias so callers importing the namespace avoid the doubled `Chat.Chat.fields`. */
|
|
70
|
+
var fields = Chat.fields;
|
|
71
|
+
var make = (props) => Obj.make(Chat, {
|
|
72
|
+
...props,
|
|
73
|
+
tasks: props.tasks ?? []
|
|
74
|
+
});
|
|
75
|
+
/**
|
|
76
|
+
* Refs to the chats accompanying an object, stored on the subject itself (replaces the former
|
|
77
|
+
* `CompanionTo` relation): the annotation is deleted with the subject, and its refs make the
|
|
78
|
+
* parented chats real children — referenced and cascaded — rather than ref-less parent-edge
|
|
79
|
+
* orphans reachable only by index query.
|
|
80
|
+
*/
|
|
81
|
+
var CompanionChatAnnotation = Annotation.make({
|
|
82
|
+
id: "org.dxos.assistant.companionChats",
|
|
83
|
+
schema: Schema.Array(Ref.Ref(Chat))
|
|
84
|
+
});
|
|
85
|
+
/**
|
|
86
|
+
* Links a chat to the subject it accompanies (a Project, an Agent, a document, ...): a ref on the
|
|
87
|
+
* subject via {@link CompanionChatAnnotation} plus the ECHO parent edge. Idempotent per chat.
|
|
88
|
+
*/
|
|
89
|
+
var linkCompanion = ({ chat, subject }) => {
|
|
90
|
+
const existing = Annotation.get(subject, CompanionChatAnnotation);
|
|
91
|
+
if (!existing.pipe(Option.getOrElse(() => [])).some((ref) => ref.uri === Ref.make(chat).uri)) {
|
|
92
|
+
if (Option.isNone(existing)) Obj.update(subject, (subject) => {
|
|
93
|
+
Annotation.set(subject, CompanionChatAnnotation, [Ref.make(chat)]);
|
|
94
|
+
});
|
|
95
|
+
else Annotation.update(subject, CompanionChatAnnotation, (chats) => {
|
|
96
|
+
chats.push(Ref.make(chat));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
Obj.setParent(chat, subject);
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Starts a chat on a default session config — a project's, say — copied rather than referenced, so
|
|
103
|
+
* a model later picked in the chat is the chat's own. A chat already carrying a config keeps it.
|
|
104
|
+
*/
|
|
105
|
+
var seedSession = (chat, session) => {
|
|
106
|
+
if (!session || chat.session) return;
|
|
107
|
+
Obj.update(chat, (chat) => {
|
|
108
|
+
chat.session = { ...session };
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
/** Creates a task the chat owns and appends it to the checklist. */
|
|
112
|
+
var addTask = (db, chat, title, props = {}) => {
|
|
113
|
+
const task = db.add(Task.make({
|
|
114
|
+
title: title.trim(),
|
|
115
|
+
status: "todo",
|
|
116
|
+
...props,
|
|
117
|
+
[Obj.Parent]: chat
|
|
118
|
+
}));
|
|
119
|
+
Obj.update(chat, (chat) => {
|
|
120
|
+
chat.tasks.push(Ref.make(task));
|
|
121
|
+
});
|
|
122
|
+
return task;
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Appends existing tasks to the checklist. Ownership is deliberately untouched — the task keeps the
|
|
126
|
+
* parent it arrived with (contrast {@link addTask}, which parents what the chat creates), so a task
|
|
127
|
+
* borrowed from a project's set stays in that set. Refs already on the checklist are skipped.
|
|
128
|
+
* Returns the refs actually appended.
|
|
129
|
+
*/
|
|
130
|
+
var assignTasks = (chat, tasks) => {
|
|
131
|
+
const added = [];
|
|
132
|
+
Obj.update(chat, (chat) => {
|
|
133
|
+
const present = new Set(chat.tasks.map((ref) => Task.refEntityId(ref)).filter((id) => id !== void 0));
|
|
134
|
+
for (const ref of tasks) {
|
|
135
|
+
const id = Task.refEntityId(ref);
|
|
136
|
+
if (id === void 0 || present.has(id)) continue;
|
|
137
|
+
present.add(id);
|
|
138
|
+
added.push(ref);
|
|
139
|
+
}
|
|
140
|
+
chat.tasks.push(...added);
|
|
141
|
+
});
|
|
142
|
+
return added;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Takes tasks off the checklist without destroying them — the inverse of {@link assignTasks}, and
|
|
146
|
+
* distinct from {@link deleteTask}, which also removes the members the chat owns. Returns the refs
|
|
147
|
+
* actually removed.
|
|
148
|
+
*/
|
|
149
|
+
var unassignTasks = (chat, tasks) => {
|
|
150
|
+
const ids = new Set(tasks.map((ref) => Task.refEntityId(ref)).filter((id) => id !== void 0));
|
|
151
|
+
const removed = chat.tasks.filter((ref) => {
|
|
152
|
+
const id = Task.refEntityId(ref);
|
|
153
|
+
return id !== void 0 && ids.has(id);
|
|
154
|
+
});
|
|
155
|
+
if (removed.length > 0) Obj.update(chat, (chat) => {
|
|
156
|
+
chat.tasks = chat.tasks.filter((ref) => {
|
|
157
|
+
const id = Task.refEntityId(ref);
|
|
158
|
+
return id === void 0 || !ids.has(id);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
return [...removed];
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Remove a task and its sub-tasks from `tasks`, the chat's checklist loaded in full (see
|
|
165
|
+
* {@link loadTasks}) — an unloaded child is invisible to the walk and would be left orphaned in
|
|
166
|
+
* the array. Returns everything dropped from the checklist.
|
|
167
|
+
*
|
|
168
|
+
* Only members the chat owns are destroyed: a delegated task belongs to the set that parents it,
|
|
169
|
+
* so taking it off this checklist must not delete it from there.
|
|
170
|
+
*/
|
|
171
|
+
var deleteTask = (db, chat, tasks, task) => {
|
|
172
|
+
const subtree = Task.subtree(tasks, task);
|
|
173
|
+
const ids = new Set(subtree.map((member) => member.id));
|
|
174
|
+
Obj.update(chat, (chat) => {
|
|
175
|
+
chat.tasks = chat.tasks.filter((ref) => {
|
|
176
|
+
const id = Task.refEntityId(ref);
|
|
177
|
+
return id === void 0 || !ids.has(id);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
for (const member of subtree) if (Obj.getParent(member)?.id === chat.id) db.remove(member);
|
|
181
|
+
return subtree;
|
|
182
|
+
};
|
|
183
|
+
/** The chat's feed entity id, readable without resolving the ref. */
|
|
184
|
+
var feedEntityId = (chat) => Task.refEntityId(chat.feed);
|
|
185
|
+
/**
|
|
186
|
+
* The chat that owns `feed`. The chat owns its feed (`SetParent`), so the parent edge answers this
|
|
187
|
+
* without a query; the scan is the fallback for a feed whose parent was never stamped.
|
|
188
|
+
*/
|
|
189
|
+
var loadForFeed = (feed) => Effect.gen(function* () {
|
|
190
|
+
const parent = Obj.getParent(feed);
|
|
191
|
+
if (parent && Obj.instanceOf(Chat, parent)) return parent;
|
|
192
|
+
return (yield* Database.query(Filter.type(Chat)).run).find((chat) => feedEntityId(chat) === feed.id);
|
|
193
|
+
}).pipe(Effect.catchCause((cause) => Effect.sync(() => {
|
|
194
|
+
log.warn("chat lookup failed; treating feed as chat-less", {
|
|
195
|
+
feed: feed.id,
|
|
196
|
+
cause: Cause.pretty(cause)
|
|
197
|
+
}, {
|
|
198
|
+
"~LogMeta": "~LogMeta",
|
|
199
|
+
F: __dxlog_file,
|
|
200
|
+
L: 238,
|
|
201
|
+
S: void 0
|
|
202
|
+
});
|
|
203
|
+
})));
|
|
204
|
+
/** Bound on the parent walk below; a conversation sits one or two edges under its project. */
|
|
205
|
+
var MAX_OWNER_DEPTH = 8;
|
|
206
|
+
/**
|
|
207
|
+
* The project a conversation belongs to, if any.
|
|
208
|
+
*
|
|
209
|
+
* Walks the parent chain rather than reading the immediate parent, because a chat reaches its project
|
|
210
|
+
* through whatever owns it — directly for a project chat, through the agent for an agent's chat.
|
|
211
|
+
*/
|
|
212
|
+
var peekProject = (chat) => {
|
|
213
|
+
let owner = Obj.getParent(chat);
|
|
214
|
+
for (let depth = 0; owner && depth < MAX_OWNER_DEPTH; depth++) {
|
|
215
|
+
if (Obj.instanceOf(Project.Project, owner)) return owner;
|
|
216
|
+
owner = Obj.getParent(owner);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
/** Loads the checklist in order, de-duplicated: a concurrent merge can land the same ref twice. */
|
|
220
|
+
var loadTasks = (chat) => Effect.gen(function* () {
|
|
221
|
+
const tasks = yield* Effect.forEach(chat.tasks, (task) => Database.load(task).pipe(Effect.orElseSucceed(() => void 0)));
|
|
222
|
+
return Task.dedupeById(tasks);
|
|
223
|
+
});
|
|
224
|
+
/** Sync twin of {@link loadTasks}; an unresolved ref contributes nothing rather than throwing. */
|
|
225
|
+
var resolveTasks = (chat) => Task.dedupeById(chat.tasks.filter((ref) => ref.isAvailable).map((ref) => ref.target));
|
|
226
|
+
/** A task is open until it reaches a terminal status. */
|
|
227
|
+
var isOpenTask = (task) => (task.status ?? "todo") === "todo" || task.status === "started";
|
|
228
|
+
/**
|
|
229
|
+
* The conversation's tasks rendered as a numbered checklist (the format the planning prompts
|
|
230
|
+
* speak), or a placeholder when none exist. Ordinals match the task list UI, and non-default
|
|
231
|
+
* status/dependencies are noted so the model can reason about readiness.
|
|
232
|
+
*/
|
|
233
|
+
var formatChecklist = (chat) => Effect.gen(function* () {
|
|
234
|
+
const tasks = yield* loadTasks(chat);
|
|
235
|
+
if (tasks.length === 0) return "No checklist found.";
|
|
236
|
+
return formatTasks(tasks);
|
|
237
|
+
});
|
|
238
|
+
/**
|
|
239
|
+
* Renders tasks as `1. [ ] Title` lines, ordinals in checklist order, each followed by an indented
|
|
240
|
+
* note line carrying the task's ref and any status/dependency notes — appended to the title, models
|
|
241
|
+
* paste the notes back into it. The ref renders as `[MNEMONIC](uri)` so the mnemonic the user reads
|
|
242
|
+
* and the handle the tools take travel together and get pasted back verbatim.
|
|
243
|
+
*/
|
|
244
|
+
var formatTasks = (tasks) => {
|
|
245
|
+
const ordinals = new Map(tasks.map((task, index) => [task.id, index + 1]));
|
|
246
|
+
return tasks.map((task, index) => {
|
|
247
|
+
const line = `${index + 1}. [${task.status === "done" ? "x" : " "}] ${task.title}`;
|
|
248
|
+
const notes = [`ref: [${Obj.getMnemonic(task)}](${Obj.getURI(task)})`];
|
|
249
|
+
if (task.status && task.status !== "todo" && task.status !== "done") notes.push(task.status);
|
|
250
|
+
const deps = (task.dependsOn ?? []).map((ref) => ref.target).filter((target) => target !== void 0).map((target) => ordinals.get(target.id) ?? target.title);
|
|
251
|
+
if (deps.length > 0) notes.push(`depends on ${deps.join(", ")}`);
|
|
252
|
+
return `${line}\n (${notes.join("; ")})`;
|
|
253
|
+
}).join("\n");
|
|
254
|
+
};
|
|
255
|
+
//#endregion
|
|
256
|
+
export { seedSession as _, assignTasks as a, fields as c, linkCompanion as d, loadForFeed as f, resolveTasks as g, peekProject as h, addTask as i, formatChecklist as l, make as m, Chat_exports as n, deleteTask as o, loadTasks as p, CompanionChatAnnotation as r, feedEntityId as s, Chat as t, isOpenTask as u, unassignTasks as v };
|
|
257
|
+
|
|
258
|
+
//# sourceMappingURL=chunk-Chat.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-Chat.mjs","names":[],"sources":["../../src/types/Chat.ts"],"sourcesContent":["//\n// Copyright 2024 DXOS.org\n//\n\n// @import-as-namespace\n\nimport * as Cause from 'effect/Cause';\nimport * as Effect from 'effect/Effect';\nimport * as Option from 'effect/Option';\nimport * as Schema from 'effect/Schema';\n\nimport { SessionConfig } from '@dxos/ai';\nimport * as Instructions from '@dxos/compute/Instructions';\nimport * as Project from '@dxos/compute/Project';\nimport { Annotation, Database, DXN, Feed, Filter, Obj, Ref, Type } from '@dxos/echo';\nimport { log } from '@dxos/log';\nimport { ArchivableAnnotation } from '@dxos/schema';\nimport { Task } from '@dxos/types';\n\n/**\n * AI chat session.\n */\n// TODO(burdon): Rename to Session?\nexport class Chat extends Type.makeObject<Chat>(DXN.make('org.dxos.type.assistant.chat', '0.2.0'))(\n Schema.Struct({\n name: Schema.String.pipe(Schema.optional),\n viewType: Schema.String.pipe(Schema.optional),\n\n /**\n * Runs this conversation's agent on the edge rather than locally, mirroring a trigger's own\n * `remote` flag. When unset, the agent runs locally on the client.\n */\n remote: Schema.Boolean.pipe(Schema.annotate({ title: 'Remote' }), Schema.optional),\n\n /** Message feed, owned by the chat so `SetParent` cascades it. */\n feed: Ref.Ref(Feed.Feed).pipe(Annotation.SetParent.set(), Annotation.FormInputAnnotation.set(false)),\n\n /**\n * Instructions steering this conversation, rendered into the system prompt at request time.\n * Held by reference (never copied), so a project's chats follow edits to its instructions.\n */\n instructions: Schema.optional(Ref.Ref(Instructions.Instructions).pipe(Annotation.FormInputAnnotation.set(false))),\n\n /**\n * How this conversation runs (its model), chosen in the chat rather than globally so it survives\n * a remount and travels with the chat. Unset fields fall back to the project's, then the agent's.\n */\n session: Schema.optional(SessionConfig.SessionConfig.pipe(Annotation.FormInputAnnotation.set(false))),\n\n /**\n * The working checklist, flat and ordered. Deliberately NOT an owning (`SetParent`) field: a\n * chat may work on a task that belongs somewhere else — a project's task set delegates one here\n * — and an owning field re-parents every resolved member on each update of the chat, which\n * would silently move that task out of the set that owns it. Tasks the chat itself creates are\n * parented to it explicitly; see {@link addTask}.\n */\n tasks: Schema.Array(Ref.Ref(Task.Task)).pipe(Annotation.FormInputAnnotation.set(false)),\n }).pipe(\n Annotation.LabelAnnotation.set(['name']),\n Annotation.IconAnnotation.set({\n icon: 'ph--sparkle--regular',\n hue: 'amber',\n }),\n Annotation.UserType.set(),\n ArchivableAnnotation.set(true),\n ),\n) {}\n\n/** Module-level alias so callers importing the namespace avoid the doubled `Chat.Chat.fields`. */\nexport const fields = Chat.fields;\n\nexport const make = (\n props: Omit<Obj.MakeProps<typeof Chat>, 'tasks'> & { tasks?: ReadonlyArray<Ref.Ref<Task.Task>> },\n): Chat => Obj.make(Chat, { ...props, tasks: props.tasks ?? [] });\n\n/**\n * Refs to the chats accompanying an object, stored on the subject itself (replaces the former\n * `CompanionTo` relation): the annotation is deleted with the subject, and its refs make the\n * parented chats real children — referenced and cascaded — rather than ref-less parent-edge\n * orphans reachable only by index query.\n */\nexport const CompanionChatAnnotation = Annotation.make({\n id: 'org.dxos.assistant.companionChats',\n schema: Schema.Array(Ref.Ref(Chat)),\n});\n\n/**\n * Links a chat to the subject it accompanies (a Project, an Agent, a document, ...): a ref on the\n * subject via {@link CompanionChatAnnotation} plus the ECHO parent edge. Idempotent per chat.\n */\nexport const linkCompanion = ({ chat, subject }: { chat: Chat; subject: Obj.Unknown }): void => {\n const existing = Annotation.get(subject, CompanionChatAnnotation);\n const chats = existing.pipe(Option.getOrElse((): readonly Ref.Ref<Chat>[] => []));\n if (!chats.some((ref) => ref.uri === Ref.make(chat).uri)) {\n if (Option.isNone(existing)) {\n Obj.update(subject, (subject) => {\n Annotation.set(subject, CompanionChatAnnotation, [Ref.make(chat)]);\n });\n } else {\n // Splice in place so only this ref is appended; Annotation.update validates the result.\n Annotation.update(subject, CompanionChatAnnotation, (chats) => {\n chats.push(Ref.make(chat));\n });\n }\n }\n Obj.setParent(chat, subject);\n};\n\n/**\n * Starts a chat on a default session config — a project's, say — copied rather than referenced, so\n * a model later picked in the chat is the chat's own. A chat already carrying a config keeps it.\n */\nexport const seedSession = (chat: Chat, session: SessionConfig.SessionConfig | undefined): void => {\n if (!session || chat.session) {\n return;\n }\n\n Obj.update(chat, (chat) => {\n chat.session = { ...session };\n });\n};\n\n/** Creates a task the chat owns and appends it to the checklist. */\nexport const addTask = (\n db: Database.Database,\n chat: Chat,\n title: string,\n props: Partial<Omit<Obj.MakeProps<typeof Task.Task>, 'title'>> = {},\n): Task.Task => {\n // Ownership is decided at creation rather than by membership, so a task the chat made cascades\n // with it while a delegated one keeps the parent it arrived with.\n const task = db.add(Task.make({ title: title.trim(), status: 'todo', ...props, [Obj.Parent]: chat }));\n Obj.update(chat, (chat) => {\n chat.tasks.push(Ref.make(task));\n });\n return task;\n};\n\n/**\n * Appends existing tasks to the checklist. Ownership is deliberately untouched — the task keeps the\n * parent it arrived with (contrast {@link addTask}, which parents what the chat creates), so a task\n * borrowed from a project's set stays in that set. Refs already on the checklist are skipped.\n * Returns the refs actually appended.\n */\nexport const assignTasks = (chat: Chat, tasks: readonly Ref.Ref<Task.Task>[]): Ref.Ref<Task.Task>[] => {\n const added: Ref.Ref<Task.Task>[] = [];\n Obj.update(chat, (chat) => {\n // Matched on entity id rather than target, so an unloaded ref still de-duplicates.\n const present = new Set(chat.tasks.map((ref) => Task.refEntityId(ref)).filter((id) => id !== undefined));\n for (const ref of tasks) {\n const id = Task.refEntityId(ref);\n if (id === undefined || present.has(id)) {\n continue;\n }\n present.add(id);\n added.push(ref);\n }\n chat.tasks.push(...added);\n });\n return added;\n};\n\n/**\n * Takes tasks off the checklist without destroying them — the inverse of {@link assignTasks}, and\n * distinct from {@link deleteTask}, which also removes the members the chat owns. Returns the refs\n * actually removed.\n */\nexport const unassignTasks = (chat: Chat, tasks: readonly Ref.Ref<Task.Task>[]): Ref.Ref<Task.Task>[] => {\n const ids = new Set(tasks.map((ref) => Task.refEntityId(ref)).filter((id) => id !== undefined));\n const removed = chat.tasks.filter((ref) => {\n const id = Task.refEntityId(ref);\n return id !== undefined && ids.has(id);\n });\n if (removed.length > 0) {\n Obj.update(chat, (chat) => {\n chat.tasks = chat.tasks.filter((ref) => {\n const id = Task.refEntityId(ref);\n return id === undefined || !ids.has(id);\n });\n });\n }\n return [...removed];\n};\n\n/**\n * Remove a task and its sub-tasks from `tasks`, the chat's checklist loaded in full (see\n * {@link loadTasks}) — an unloaded child is invisible to the walk and would be left orphaned in\n * the array. Returns everything dropped from the checklist.\n *\n * Only members the chat owns are destroyed: a delegated task belongs to the set that parents it,\n * so taking it off this checklist must not delete it from there.\n */\nexport const deleteTask = (\n db: Database.Database,\n chat: Chat,\n tasks: readonly Task.Task[],\n task: Task.Task,\n): Task.Task[] => {\n const subtree = Task.subtree(tasks, task);\n const ids = new Set(subtree.map((member) => member.id));\n Obj.update(chat, (chat) => {\n // Matched on the ref's own entity id rather than its target, so an entry whose object is not\n // loaded is still swept.\n chat.tasks = chat.tasks.filter((ref) => {\n const id = Task.refEntityId(ref);\n return id === undefined || !ids.has(id);\n });\n });\n for (const member of subtree) {\n if (Obj.getParent(member)?.id === chat.id) {\n db.remove(member);\n }\n }\n return subtree;\n};\n\n/** The chat's feed entity id, readable without resolving the ref. */\nexport const feedEntityId = (chat: Chat): string | undefined => Task.refEntityId(chat.feed);\n\n/**\n * The chat that owns `feed`. The chat owns its feed (`SetParent`), so the parent edge answers this\n * without a query; the scan is the fallback for a feed whose parent was never stamped.\n */\nexport const loadForFeed = (feed: Feed.Feed): Effect.Effect<Chat | undefined, never, Database.Service> =>\n Effect.gen(function* () {\n const parent = Obj.getParent(feed);\n if (parent && Obj.instanceOf(Chat, parent)) {\n return parent;\n }\n const chats = yield* Database.query(Filter.type(Chat)).run;\n return chats.find((chat) => feedEntityId(chat) === feed.id);\n // A chat-less feed is legitimate (a bare `AiSession`, or a database with the type unregistered),\n // so callers get `undefined` rather than a failure — but a query that actually broke is logged,\n // since it is otherwise indistinguishable from the feed simply having no chat.\n }).pipe(\n Effect.catchCause((cause) =>\n Effect.sync(() => {\n log.warn('chat lookup failed; treating feed as chat-less', { feed: feed.id, cause: Cause.pretty(cause) });\n return undefined;\n }),\n ),\n );\n\n/** Bound on the parent walk below; a conversation sits one or two edges under its project. */\nconst MAX_OWNER_DEPTH = 8;\n\n/**\n * The project a conversation belongs to, if any.\n *\n * Walks the parent chain rather than reading the immediate parent, because a chat reaches its project\n * through whatever owns it — directly for a project chat, through the agent for an agent's chat.\n */\nexport const peekProject = (chat: Chat): Project.Project | undefined => {\n let owner: Obj.Unknown | undefined = Obj.getParent(chat);\n for (let depth = 0; owner && depth < MAX_OWNER_DEPTH; depth++) {\n if (Obj.instanceOf(Project.Project, owner)) {\n return owner;\n }\n owner = Obj.getParent(owner);\n }\n return undefined;\n};\n\n/** Loads the checklist in order, de-duplicated: a concurrent merge can land the same ref twice. */\nexport const loadTasks = (chat: Chat): Effect.Effect<Task.Task[], never, Database.Service> =>\n Effect.gen(function* () {\n const tasks = yield* Effect.forEach(chat.tasks, (task) =>\n Database.load(task).pipe(Effect.orElseSucceed(() => undefined)),\n );\n\n return Task.dedupeById(tasks);\n });\n\n/** Sync twin of {@link loadTasks}; an unresolved ref contributes nothing rather than throwing. */\nexport const resolveTasks = (chat: Chat): Task.Task[] =>\n Task.dedupeById(chat.tasks.filter((ref) => ref.isAvailable).map((ref) => ref.target));\n\n/** A task is open until it reaches a terminal status. */\nexport const isOpenTask = (task: Task.Task): boolean => (task.status ?? 'todo') === 'todo' || task.status === 'started';\n\n/**\n * The conversation's tasks rendered as a numbered checklist (the format the planning prompts\n * speak), or a placeholder when none exist. Ordinals match the task list UI, and non-default\n * status/dependencies are noted so the model can reason about readiness.\n */\nexport const formatChecklist = (chat: Chat): Effect.Effect<string, never, Database.Service> =>\n Effect.gen(function* () {\n const tasks = yield* loadTasks(chat);\n if (tasks.length === 0) {\n return 'No checklist found.';\n }\n\n return formatTasks(tasks);\n });\n\n/**\n * Renders tasks as `1. [ ] Title` lines, ordinals in checklist order, each followed by an indented\n * note line carrying the task's ref and any status/dependency notes — appended to the title, models\n * paste the notes back into it. The ref renders as `[MNEMONIC](uri)` so the mnemonic the user reads\n * and the handle the tools take travel together and get pasted back verbatim.\n */\nconst formatTasks = (tasks: readonly Task.Task[]): string => {\n const ordinals = new Map(tasks.map((task, index) => [task.id, index + 1]));\n return tasks\n .map((task, index) => {\n const line = `${index + 1}. [${task.status === 'done' ? 'x' : ' '}] ${task.title}`;\n // The ref is the handle update-tasks takes, so every line carries one the model can pass back.\n const notes: string[] = [`ref: [${Obj.getMnemonic(task)}](${Obj.getURI(task)})`];\n if (task.status && task.status !== 'todo' && task.status !== 'done') {\n notes.push(task.status);\n }\n\n const deps = (task.dependsOn ?? [])\n .map((ref) => ref.target)\n .filter((target) => target !== undefined)\n .map((target) => ordinals.get(target.id) ?? target.title);\n if (deps.length > 0) {\n notes.push(`depends on ${deps.join(', ')}`);\n }\n\n return `${line}\\n (${notes.join('; ')})`;\n })\n .join('\\n');\n};\n"],"mappings":"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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { t as __exportAll } from "./chunk-rolldown-runtime.mjs";
|
|
2
|
-
import { Binder } from "./
|
|
3
|
-
import { loadForFeed } from "./
|
|
4
|
-
import {
|
|
5
|
-
import { SessionLink } from "./
|
|
2
|
+
import { n as Binder } from "./chunk-AiContext.mjs";
|
|
3
|
+
import { f as loadForFeed } from "./chunk-Chat.mjs";
|
|
4
|
+
import { i as make$1, t as Alarm } from "./chunk-Alarm.mjs";
|
|
5
|
+
import { t as SessionLink } from "./chunk-SessionLink.mjs";
|
|
6
6
|
import * as Context from "effect/Context";
|
|
7
7
|
import * as Effect from "effect/Effect";
|
|
8
8
|
import * as Layer from "effect/Layer";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chunk-Harness.mjs","names":[],"sources":["../../src/session/SessionStore.ts","../../src/session/Harness.ts"],"sourcesContent":["//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\nimport * as Option from 'effect/Option';\nimport * as Schema from 'effect/Schema';\n\nimport { Annotation, Database, Feed, Filter, Obj } from '@dxos/echo';\nimport { Message } from '@dxos/types';\n\nimport * as Alarm from './Alarm.ts';\nimport * as SessionLink from './SessionLink.ts';\n\n/**\n * Marks a feed `Message` as queued agent input, pending processing. A queued message is the queue\n * entry, not a turn, so it never enters conversation history — the turn the agent runs from it\n * appends its own user message, which is what history shows.\n */\nexport const QueuedAnnotation: Annotation.Annotation<boolean> = Annotation.make({\n id: 'org.dxos.annotation.queued',\n schema: Schema.Boolean,\n});\n\n/**\n * Marks a queued item (a `Message` or an {@link Alarm.Alarm}) as taken up and finished with.\n *\n * Written by the agent AFTER the turn it drove, never before: the mark is the ack in a queue, and an\n * ack written up front would drop the item on a process that dies mid-turn. Because it lands late,\n * an interrupted turn is redelivered — at-least-once, matching how tool results already behave.\n */\nexport const ConsumedAnnotation: Annotation.Annotation<boolean> = Annotation.make({\n id: 'org.dxos.annotation.consumed',\n schema: Schema.Boolean,\n});\n\n/**\n * Marks a queued item as taken up by the turn currently running.\n *\n * Distinct from {@link ConsumedAnnotation}, which cannot serve this purpose: the ack lands only after\n * the turn, so between the agent dequeuing an entry and finishing with it the entry is still pending\n * — and the turn has meanwhile appended its own user message built from the entry's blocks, leaving\n * the same content rendered in both the queue and the thread. An in-flight entry stays in the pending\n * set, so a process that dies mid-turn still redelivers it; it is only held out of the queue view,\n * which the thread now speaks for.\n */\nexport const InFlightAnnotation: Annotation.Annotation<boolean> = Annotation.make({\n id: 'org.dxos.annotation.inFlight',\n schema: Schema.Boolean,\n});\n\nexport const isQueued = (item: Obj.Unknown | Obj.Snapshot): boolean =>\n Option.getOrElse(Annotation.get(item, QueuedAnnotation), () => false);\n\nexport const isConsumed = (item: Obj.Unknown | Obj.Snapshot): boolean =>\n Option.getOrElse(Annotation.get(item, ConsumedAnnotation), () => false);\n\nexport const isInFlight = (item: Obj.Unknown | Obj.Snapshot): boolean =>\n Option.getOrElse(Annotation.get(item, InFlightAnnotation), () => false);\n\n/**\n * The pending (queued, un-acked) portion of a session's feed state.\n */\nexport interface PendingState {\n /** Queued, un-acked input messages in feed order. */\n pendingMessages: Message.Message[];\n /** Un-acked alarms, earliest `wakeAt` first. */\n pendingAlarms: Alarm.Alarm[];\n}\n\n/**\n * The session state reconstructed from the feed for the next turn.\n */\nexport interface SessionState extends PendingState {\n /** The conversation as the model should see it (queued originals excluded, fork history reified). */\n history: Message.Message[];\n}\n\nexport type SetAlarmProps = Alarm.MakeProps;\n\n/**\n * Maps between a session feed and reified session state: conversation history (resolving\n * `SessionLink` fork records), the pending input queue, and pending alarms — plus the writes that\n * advance that state. The pending queue is a projection: an item is pending until it is marked with\n * {@link ConsumedAnnotation}; consumed items stay in the feed, and `Feed.remove` is only the\n * cancellation of a still-pending item.\n */\nexport class SessionStore {\n /**\n * Reconstructs {@link PendingState} with a single feed query and one linear scan over its items.\n */\n loadPending(feed: Feed.Feed): Effect.Effect<PendingState, never, Database.Service> {\n return Effect.map(this.#scan(feed), ({ pendingMessages, pendingAlarms }) => ({ pendingMessages, pendingAlarms }));\n }\n\n /**\n * Reconstructs {@link SessionState}: the linear scan of {@link loadPending} plus reified history.\n */\n loadState(feed: Feed.Feed): Effect.Effect<SessionState, never, Database.Service> {\n return Effect.gen({ self: this }, function* () {\n const { ordered, pendingMessages, pendingAlarms } = yield* this.#scan(feed);\n const { items: reachable } = Feed.history(ordered.filter((message) => !isQueued(message)));\n const history = yield* this.reifyHistory(feed, reachable);\n return { history, pendingMessages, pendingAlarms };\n });\n }\n\n /** One feed query, one linear scan: partitions items and projects the pending sets. */\n #scan(feed: Feed.Feed): Effect.Effect<PendingState & { ordered: Message.Message[] }, never, Database.Service> {\n return Effect.gen(function* () {\n // The two record kinds by type rather than `Filter.everything()`: the queue only cares about\n // these, and an everything-query also drags in the tombstone a cancellation leaves behind.\n const items = yield* Feed.query(feed, Filter.or(Filter.type(Message.Message), Filter.type(Alarm.Alarm))).run;\n\n const messages: Message.Message[] = [];\n const alarms: Alarm.Alarm[] = [];\n for (const item of items) {\n // A cancelled entry is removed from the feed, and `Feed.remove` leaves a tombstone that keeps\n // the item's type and body (so it still looks queued) — skip it or a cancellation never takes.\n if (Obj.isDeleted(item)) {\n continue;\n }\n if (Obj.instanceOf(Message.Message, item)) {\n messages.push(item);\n } else if (Obj.instanceOf(Alarm.Alarm, item)) {\n alarms.push(item);\n }\n }\n\n const ordered = [...messages].sort(byFeedPosition);\n const pendingMessages = ordered.filter((message) => isQueued(message) && !isConsumed(message));\n const pendingAlarms = alarms.filter((alarm) => !isConsumed(alarm)).sort((a, b) => a.wakeAt - b.wakeAt);\n\n return { ordered, pendingMessages, pendingAlarms };\n });\n }\n\n /**\n * Prepends linked history to `messages` when the feed contains a SessionLink.\n * Queries the feed for any SessionLink record; if found, loads messages from\n * the referenced feed up to and including `messageId` and prepends them.\n * Queued entries never enter history — the turn driven from one appends its own user message.\n */\n reifyHistory(\n feed: Feed.Feed,\n messages: Message.Message[],\n ): Effect.Effect<Message.Message[], never, Database.Service> {\n const current = messages.filter((message) => !isQueued(message));\n return Effect.gen(function* () {\n const links = yield* Feed.query(feed, Filter.type(SessionLink.SessionLink)).run;\n const sessionLinks = links.filter(Obj.instanceOf(SessionLink.SessionLink));\n\n const link = sessionLinks[0];\n if (!link) {\n return current;\n }\n\n const sourceFeed = link.feedRef.target;\n if (!sourceFeed) {\n return current;\n }\n\n const sourceMessages = yield* Feed.query(sourceFeed, Filter.type(Message.Message)).run;\n const filtered = sourceMessages.filter(Obj.instanceOf(Message.Message)).filter((message) => !isQueued(message));\n\n // Sort by creation timestamp so history is in chronological order.\n const sorted = [...filtered].sort((a, b) => a.created.localeCompare(b.created));\n\n // Include messages up to and including the fork point.\n const cutoffIndex = sorted.findIndex((m) => m.id === link.messageId);\n if (cutoffIndex < 0) {\n // Fork point not found; return original messages unmodified to avoid injecting unexpected history.\n return current;\n }\n\n return [...sorted.slice(0, cutoffIndex + 1), ...current];\n });\n }\n\n /**\n * Appends `message` to the feed as queued input (pending until acked or removed).\n */\n enqueueMessage(feed: Feed.Feed, message: Message.Message): Effect.Effect<Message.Message, never, Database.Service> {\n Obj.update(message, (message) => Annotation.set(message, QueuedAnnotation, true));\n return Feed.append(feed, [message]).pipe(Effect.as(message));\n }\n\n /**\n * Marks a queued item as consumed, taking it out of the pending projection. Re-appending an item\n * by id is an upsert, so this records the mark without adding a record to the log.\n *\n * Call it AFTER the work the item drove: the pending set is what a rehydrated process reads, so an\n * item marked early is an item silently dropped when that process dies mid-turn.\n */\n /**\n * Marks a queued item as taken up by the running turn, taking it out of the queue view without\n * taking it out of the pending set (see {@link InFlightAnnotation}).\n *\n * Call it when the item is dequeued, BEFORE the work it drives — the opposite of {@link ack}.\n */\n markInFlight(feed: Feed.Feed, item: Message.Message | Alarm.Alarm): Effect.Effect<void, never, Database.Service> {\n Obj.update(item, (item) => Annotation.set(item, InFlightAnnotation, true));\n return Feed.append(feed, [item]).pipe(Effect.asVoid);\n }\n\n ack(feed: Feed.Feed, item: Message.Message | Alarm.Alarm): Effect.Effect<void, never, Database.Service> {\n Obj.update(item, (item) => Annotation.set(item, ConsumedAnnotation, true));\n return Feed.append(feed, [item]).pipe(Effect.asVoid);\n }\n\n /**\n * Schedules an alarm by appending an {@link Alarm.Alarm} record. Multiple alarms may be pending.\n */\n setAlarm(feed: Feed.Feed, props: SetAlarmProps): Effect.Effect<Alarm.Alarm, never, Database.Service> {\n const alarm = Alarm.make(props);\n return Feed.append(feed, [alarm]).pipe(Effect.as(alarm));\n }\n\n /**\n * Cancels a pending alarm (plain feed CRUD; an already-acked alarm needs no cancellation).\n */\n cancelAlarm(feed: Feed.Feed, alarm: Alarm.Alarm): Effect.Effect<void, never, Database.Service> {\n return Feed.remove(feed, [alarm]);\n }\n}\n\n/**\n * Orders feed items by the position the server assigned them. Unpositioned blocks (written locally\n * and not yet acknowledged) sort last: a message just written is the newest.\n */\nconst byFeedPosition = (a: Message.Message, b: Message.Message): number => {\n const positionA = Feed.getPosition(a);\n const positionB = Feed.getPosition(b);\n return positionA === positionB ? 0 : positionA < positionB ? -1 : 1;\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\n// @import-as-namespace\n\nimport * as Context from 'effect/Context';\nimport * as DateTime from 'effect/DateTime';\nimport * as Effect from 'effect/Effect';\nimport * as Layer from 'effect/Layer';\nimport * as Option from 'effect/Option';\nimport type * as RpcClient from 'effect/rpc/RpcClient';\nimport type * as Scope from 'effect/Scope';\n\nimport { ProcessManager } from '@dxos/compute-runtime';\nimport * as LayerSpec from '@dxos/compute/LayerSpec';\nimport * as Process from '@dxos/compute/Process';\nimport * as ServiceResolver from '@dxos/compute/ServiceResolver';\nimport { Annotation, Database, EID, Feed, Filter, Obj, type URI } from '@dxos/echo';\nimport * as EffectEx from '@dxos/effect/EffectEx';\nimport { BaseError } from '@dxos/errors';\nimport { type ContentBlock, Message } from '@dxos/types';\n\nimport * as Chat from '../types/Chat.ts';\nimport * as AiContext from './AiContext.ts';\nimport { type HarnessControlRpcs } from './harness-control.ts';\nimport { SessionStore } from './SessionStore.ts';\n\nexport interface Service {\n /** The conversation {@link AiContext.Binder} (Tier A). */\n binder: Effect.Effect<AiContext.Binder, NotSupportedError>;\n /** The {@link Chat.Chat} the conversation belongs to (Tier A). */\n chat: Effect.Effect<Chat.Chat, NotSupportedError>;\n /** The conversation message history (Tier A). */\n history: Effect.Effect<Message.Message[], NotSupportedError>;\n /** Objects bound to the conversation that match the filter (Tier A). */\n queryContext<T extends Obj.Unknown>(filter: Filter.Filter<T>): Effect.Effect<T[], NotSupportedError>;\n /** Enqueue a message into the owning host's input queue (Tier B). */\n enqueueMessage(options: EnqueueMessageOptions): Effect.Effect<void, NotSupportedError>;\n /** Schedule a self-wake on the owning host (Tier B). */\n setAlarm(options: SetAlarmOptions): Effect.Effect<void, NotSupportedError>;\n}\n\n/**\n * Provides access to the controlling harness for tools and operations that are executed by the agent.\n *\n * Replaces AiContextService and AiSessionService.\n */\nexport class HarnessService extends Context.Service<HarnessService, Service>()('@dxos/assistant/HarnessService') {}\n\n/**\n * Acess current context binder.\n */\nexport const binder: Effect.Effect<AiContext.Binder, NotSupportedError, HarnessService> = Effect.flatMap(\n HarnessService,\n (service) => service.binder,\n);\n\n/**\n * The chat the conversation belongs to.\n *\n * The agent process is bound to its chat (its spawn target), so session-scoped tools resolve the\n * conversation's plan, checklist and instructions from here rather than by looking for a Chat among\n * the bound context objects.\n */\nexport const getChat: Effect.Effect<Chat.Chat, NotSupportedError, HarnessService> = Effect.flatMap(\n HarnessService,\n (service) => service.chat,\n);\n\n/**\n * Query the context using a filter.\n */\nexport const queryContext = <T extends Obj.Unknown>(\n filter: Filter.Filter<T>,\n): Effect.Effect<T[], NotSupportedError, HarnessService> =>\n Effect.flatMap(HarnessService, (service) => service.queryContext(filter));\n\ninterface EnqueueMessageOptions {\n content: ContentBlock.Any[];\n\n // TODO(dmaretskyi): Order: 'first' | 'last'\n}\n\n/**\n * Query the session history.\n */\nexport const history: Effect.Effect<Message.Message[], NotSupportedError, HarnessService> = Effect.flatMap(\n HarnessService,\n (service) => service.history,\n);\n\n/**\n * Enqueue a message to the harness.\n */\nexport const enqueueMessage = (\n options: EnqueueMessageOptions,\n): Effect.Effect<void, NotSupportedError, HarnessService> =>\n Effect.flatMap(HarnessService, (service) => service.enqueueMessage(options));\n\ninterface SetAlarmOptions {\n at: DateTime.DateTime;\n /**\n * Message to send when the alarm fires.\n */\n message: string | null;\n}\n\nexport const setAlarm = (options: SetAlarmOptions): Effect.Effect<void, NotSupportedError, HarnessService> =>\n Effect.flatMap(HarnessService, (service) => service.setAlarm(options));\n\n/**\n * Binds skills and/or objects to the conversation context.\n */\nexport const bindContext = ({\n skills,\n objects,\n}: AiContext.BindingProps): Effect.Effect<void, NotSupportedError, HarnessService> =>\n Effect.gen(function* () {\n const sessionBinder = yield* binder;\n yield* Effect.promise(() => sessionBinder.bind({ skills, objects }));\n });\n\nexport class NotSupportedError extends BaseError.extend('NotSupportedError', 'Operation not supported') {}\n\n/**\n * Process-affinity {@link LayerSpec} that materialises {@link HarnessService} from the resolution\n * context's `conversation` DXN. Merges the former `AiContext.Service` (Tier A) with live host\n * dispatch (Tier B).\n */\nexport const layerSpec: LayerSpec.LayerSpec = LayerSpec.make(\n {\n affinity: 'process',\n requires: [Database.Service, ProcessManager.Service],\n provides: [HarnessService],\n },\n (context) =>\n Layer.effect(\n HarnessService,\n Effect.gen(function* () {\n if (!context.conversation) {\n // LayerSpec.make requires a never-failing layer (error channel = never), so typed failure\n // is not possible here. Die with ServiceNotAvailableError to signal a programming error\n // (missing 'conversation' in spawn environment) that callers cannot recover from.\n return yield* Effect.die(\n new ServiceResolver.ServiceNotAvailableError(HarnessService.key, {\n message: `Service not available: ${HarnessService.key} — process spawn is missing 'conversation' in environment (set via Operation.withInvocationOptions or ProcessManager.spawn environment)`,\n }),\n );\n }\n const conversation = context.conversation;\n const processManager = yield* ProcessManager.Service;\n const runtime = yield* Effect.context<Database.Service>();\n return yield* make({ conversation, processManager, runtime });\n }),\n ),\n);\n\ninterface MakeOptions {\n conversation: URI.URI;\n processManager: Context.Service.Shape<typeof ProcessManager.Service>;\n runtime: Context.Context<Database.Service>;\n}\n\n/**\n * Builds the {@link Service} for a conversation-scoped resolution context. Tier A resolves the\n * conversation feed / {@link AiContext.Binder} once; Tier B looks up the owning host process lazily per call\n * and dispatches over its `Handle.rpc` control surface (raising {@link NotSupportedError} when no\n * live host owns the conversation).\n */\nexport const make = ({\n conversation,\n processManager,\n runtime,\n}: MakeOptions): Effect.Effect<Service, never, Scope.Scope> =>\n Effect.gen(function* () {\n const feed = yield* Database.resolve(EID.parse(conversation), Feed.Feed).pipe(\n Effect.provide(runtime),\n Effect.orDie,\n );\n const boundBinder = yield* EffectEx.acquireReleaseResource(() => new AiContext.Binder({ feed, runtime }));\n // Cached rather than resolved up front: a harness is built for every operation the agent\n // invokes, and most never ask for the chat — resolving one here would put a lookup on the tool\n // path. The agent process is spawned against the chat, so the host is discovered by the chat's\n // URI; a feed with no chat (e.g. a bare `AiSession`) keeps the feed as its own host target.\n const chat = yield* Effect.cached(Chat.loadForFeed(feed).pipe(Effect.provide(runtime)));\n return makeService({\n feed,\n chat: chatOrFail(chat),\n runtime,\n binder: boundBinder,\n owningHost: Effect.flatMap(chat, (chat) =>\n lookupOwningHost(processManager, chat ? Obj.getURI(chat) : conversation),\n ),\n });\n });\n\ninterface FromBinderOptions {\n feed: Feed.Feed;\n runtime: Context.Context<Database.Service>;\n binder: AiContext.Binder;\n}\n\n/**\n * Builds the {@link Service} from a {@link AiContext.Binder} already opened for the conversation (e.g. the\n * agent's own turn fiber, which has no `ProcessManager.Service` in scope). Tier A is fully served;\n * Tier B raises {@link NotSupportedError} since the live-host control surface is not reachable here.\n */\nexport const fromBinder = ({ feed, runtime, binder }: FromBinderOptions): Service =>\n makeService({\n feed,\n // Resolved per call rather than up front: this is the synchronous constructor, and the chat is\n // only needed by the tools that ask for it.\n chat: chatOrFail(Chat.loadForFeed(feed).pipe(Effect.provide(runtime))),\n runtime,\n binder,\n owningHost: Effect.fail(new NotSupportedError()),\n });\n\ninterface MakeServiceOptions {\n feed: Feed.Feed;\n chat: Effect.Effect<Chat.Chat, NotSupportedError>;\n runtime: Context.Context<Database.Service>;\n binder: AiContext.Binder;\n owningHost: Effect.Effect<RpcClient.RpcClient<HarnessControlRpcs>, NotSupportedError>;\n}\n\nconst makeService = ({ feed, chat, runtime, binder, owningHost }: MakeServiceOptions): Service => ({\n binder: Effect.succeed(binder),\n chat,\n history: Effect.gen(function* () {\n const messages = yield* Feed.query(feed, Filter.type(Message.Message)).run;\n return yield* new SessionStore().reifyHistory(feed, messages);\n }).pipe(Effect.provide(runtime)),\n queryContext: <T extends Obj.Unknown>(filter: Filter.Filter<T>) =>\n Effect.sync(() => {\n const match = Filter.toPredicate(filter);\n return binder.getObjects().filter(match);\n }),\n enqueueMessage: ({ content }) => owningHost.pipe(Effect.flatMap((rpc) => rpc.enqueueMessage({ content }))),\n setAlarm: ({ at, message }) =>\n owningHost.pipe(Effect.flatMap((rpc) => rpc.setAlarm({ at: DateTime.toUtc(at), message }))),\n});\n\n/** A chat-less conversation (e.g. a bare `AiSession` feed) has no chat to hand out. */\nconst chatOrFail = (chat: Effect.Effect<Chat.Chat | undefined, never>): Effect.Effect<Chat.Chat, NotSupportedError> =>\n Effect.flatMap(chat, (chat) => (chat ? Effect.succeed(chat) : Effect.fail(new NotSupportedError())));\n\n/**\n * Resolves the live host owning `conversation` per call (so a process replacement — e.g. a model\n * switch — is never captured stale) and exposes its `HarnessControl` RPC client.\n */\nconst lookupOwningHost = (\n processManager: Context.Service.Shape<typeof ProcessManager.Service>,\n conversation: URI.URI,\n): Effect.Effect<RpcClient.RpcClient<HarnessControlRpcs>, NotSupportedError> =>\n Effect.gen(function* () {\n const processes = yield* processManager.list({ target: conversation });\n const host = processes.find(\n (process) =>\n !Process.isTerminal(process.status.state) &&\n Option.getOrElse(\n Annotation.getDictionary(process.params.annotations, Process.HarnessHostAnnotation),\n () => false,\n ),\n );\n if (!host) {\n return yield* Effect.fail(new NotSupportedError());\n }\n // The host is discovered by annotation, but the HarnessControl RPC contract is fixed at the\n // spawn site (agent-process declares it as its `rpcs`); narrow the variance-erased\n // `RpcClient<any>` to that contract so the dispatch below is typed. No typed path exists here:\n // the host handle carries `RpcClient<any>` (design spec §4.4) and the concrete definition lives\n // in `@dxos/functions-runtime`, which depends on this package — importing it would cycle.\n return host.rpc as unknown as RpcClient.RpcClient<HarnessControlRpcs>;\n });\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chunk-Harness.mjs","names":["Alarm.Alarm","SessionLink.SessionLink","Alarm.make","AiContext.Binder","Chat.loadForFeed"],"sources":["../../src/session/SessionStore.ts","../../src/session/Harness.ts"],"sourcesContent":["//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\nimport * as Option from 'effect/Option';\nimport * as Schema from 'effect/Schema';\n\nimport { Annotation, Database, Feed, Filter, Obj } from '@dxos/echo';\nimport { Message } from '@dxos/types';\n\nimport * as Alarm from './Alarm.ts';\nimport * as SessionLink from './SessionLink.ts';\n\n/**\n * Marks a feed `Message` as queued agent input, pending processing. A queued message is the queue\n * entry, not a turn, so it never enters conversation history — the turn the agent runs from it\n * appends its own user message, which is what history shows.\n */\nexport const QueuedAnnotation: Annotation.Annotation<boolean> = Annotation.make({\n id: 'org.dxos.annotation.queued',\n schema: Schema.Boolean,\n});\n\n/**\n * Marks a queued item (a `Message` or an {@link Alarm.Alarm}) as taken up and finished with.\n *\n * Written by the agent AFTER the turn it drove, never before: the mark is the ack in a queue, and an\n * ack written up front would drop the item on a process that dies mid-turn. Because it lands late,\n * an interrupted turn is redelivered — at-least-once, matching how tool results already behave.\n */\nexport const ConsumedAnnotation: Annotation.Annotation<boolean> = Annotation.make({\n id: 'org.dxos.annotation.consumed',\n schema: Schema.Boolean,\n});\n\n/**\n * Marks a queued item as taken up by the turn currently running.\n *\n * Distinct from {@link ConsumedAnnotation}, which cannot serve this purpose: the ack lands only after\n * the turn, so between the agent dequeuing an entry and finishing with it the entry is still pending\n * — and the turn has meanwhile appended its own user message built from the entry's blocks, leaving\n * the same content rendered in both the queue and the thread. An in-flight entry stays in the pending\n * set, so a process that dies mid-turn still redelivers it; it is only held out of the queue view,\n * which the thread now speaks for.\n */\nexport const InFlightAnnotation: Annotation.Annotation<boolean> = Annotation.make({\n id: 'org.dxos.annotation.inFlight',\n schema: Schema.Boolean,\n});\n\nexport const isQueued = (item: Obj.Unknown | Obj.Snapshot): boolean =>\n Option.getOrElse(Annotation.get(item, QueuedAnnotation), () => false);\n\nexport const isConsumed = (item: Obj.Unknown | Obj.Snapshot): boolean =>\n Option.getOrElse(Annotation.get(item, ConsumedAnnotation), () => false);\n\nexport const isInFlight = (item: Obj.Unknown | Obj.Snapshot): boolean =>\n Option.getOrElse(Annotation.get(item, InFlightAnnotation), () => false);\n\n/**\n * The pending (queued, un-acked) portion of a session's feed state.\n */\nexport interface PendingState {\n /** Queued, un-acked input messages in feed order. */\n pendingMessages: Message.Message[];\n /** Un-acked alarms, earliest `wakeAt` first. */\n pendingAlarms: Alarm.Alarm[];\n}\n\n/**\n * The session state reconstructed from the feed for the next turn.\n */\nexport interface SessionState extends PendingState {\n /** The conversation as the model should see it (queued originals excluded, fork history reified). */\n history: Message.Message[];\n}\n\nexport type SetAlarmProps = Alarm.MakeProps;\n\n/**\n * Maps between a session feed and reified session state: conversation history (resolving\n * `SessionLink` fork records), the pending input queue, and pending alarms — plus the writes that\n * advance that state. The pending queue is a projection: an item is pending until it is marked with\n * {@link ConsumedAnnotation}; consumed items stay in the feed, and `Feed.remove` is only the\n * cancellation of a still-pending item.\n */\nexport class SessionStore {\n /**\n * Reconstructs {@link PendingState} with a single feed query and one linear scan over its items.\n */\n loadPending(feed: Feed.Feed): Effect.Effect<PendingState, never, Database.Service> {\n return Effect.map(this.#scan(feed), ({ pendingMessages, pendingAlarms }) => ({ pendingMessages, pendingAlarms }));\n }\n\n /**\n * Reconstructs {@link SessionState}: the linear scan of {@link loadPending} plus reified history.\n */\n loadState(feed: Feed.Feed): Effect.Effect<SessionState, never, Database.Service> {\n return Effect.gen({ self: this }, function* () {\n const { ordered, pendingMessages, pendingAlarms } = yield* this.#scan(feed);\n const { items: reachable } = Feed.history(ordered.filter((message) => !isQueued(message)));\n const history = yield* this.reifyHistory(feed, reachable);\n return { history, pendingMessages, pendingAlarms };\n });\n }\n\n /** One feed query, one linear scan: partitions items and projects the pending sets. */\n #scan(feed: Feed.Feed): Effect.Effect<PendingState & { ordered: Message.Message[] }, never, Database.Service> {\n return Effect.gen(function* () {\n // The two record kinds by type rather than `Filter.everything()`: the queue only cares about\n // these, and an everything-query also drags in the tombstone a cancellation leaves behind.\n const items = yield* Feed.query(feed, Filter.or(Filter.type(Message.Message), Filter.type(Alarm.Alarm))).run;\n\n const messages: Message.Message[] = [];\n const alarms: Alarm.Alarm[] = [];\n for (const item of items) {\n // A cancelled entry is removed from the feed, and `Feed.remove` leaves a tombstone that keeps\n // the item's type and body (so it still looks queued) — skip it or a cancellation never takes.\n if (Obj.isDeleted(item)) {\n continue;\n }\n if (Obj.instanceOf(Message.Message, item)) {\n messages.push(item);\n } else if (Obj.instanceOf(Alarm.Alarm, item)) {\n alarms.push(item);\n }\n }\n\n const ordered = [...messages].sort(byFeedPosition);\n const pendingMessages = ordered.filter((message) => isQueued(message) && !isConsumed(message));\n const pendingAlarms = alarms.filter((alarm) => !isConsumed(alarm)).sort((a, b) => a.wakeAt - b.wakeAt);\n\n return { ordered, pendingMessages, pendingAlarms };\n });\n }\n\n /**\n * Prepends linked history to `messages` when the feed contains a SessionLink.\n * Queries the feed for any SessionLink record; if found, loads messages from\n * the referenced feed up to and including `messageId` and prepends them.\n * Queued entries never enter history — the turn driven from one appends its own user message.\n */\n reifyHistory(\n feed: Feed.Feed,\n messages: Message.Message[],\n ): Effect.Effect<Message.Message[], never, Database.Service> {\n const current = messages.filter((message) => !isQueued(message));\n return Effect.gen(function* () {\n const links = yield* Feed.query(feed, Filter.type(SessionLink.SessionLink)).run;\n const sessionLinks = links.filter(Obj.instanceOf(SessionLink.SessionLink));\n\n const link = sessionLinks[0];\n if (!link) {\n return current;\n }\n\n const sourceFeed = link.feedRef.target;\n if (!sourceFeed) {\n return current;\n }\n\n const sourceMessages = yield* Feed.query(sourceFeed, Filter.type(Message.Message)).run;\n const filtered = sourceMessages.filter(Obj.instanceOf(Message.Message)).filter((message) => !isQueued(message));\n\n // Sort by creation timestamp so history is in chronological order.\n const sorted = [...filtered].sort((a, b) => a.created.localeCompare(b.created));\n\n // Include messages up to and including the fork point.\n const cutoffIndex = sorted.findIndex((m) => m.id === link.messageId);\n if (cutoffIndex < 0) {\n // Fork point not found; return original messages unmodified to avoid injecting unexpected history.\n return current;\n }\n\n return [...sorted.slice(0, cutoffIndex + 1), ...current];\n });\n }\n\n /**\n * Appends `message` to the feed as queued input (pending until acked or removed).\n */\n enqueueMessage(feed: Feed.Feed, message: Message.Message): Effect.Effect<Message.Message, never, Database.Service> {\n Obj.update(message, (message) => Annotation.set(message, QueuedAnnotation, true));\n return Feed.append(feed, [message]).pipe(Effect.as(message));\n }\n\n /**\n * Marks a queued item as consumed, taking it out of the pending projection. Re-appending an item\n * by id is an upsert, so this records the mark without adding a record to the log.\n *\n * Call it AFTER the work the item drove: the pending set is what a rehydrated process reads, so an\n * item marked early is an item silently dropped when that process dies mid-turn.\n */\n /**\n * Marks a queued item as taken up by the running turn, taking it out of the queue view without\n * taking it out of the pending set (see {@link InFlightAnnotation}).\n *\n * Call it when the item is dequeued, BEFORE the work it drives — the opposite of {@link ack}.\n */\n markInFlight(feed: Feed.Feed, item: Message.Message | Alarm.Alarm): Effect.Effect<void, never, Database.Service> {\n Obj.update(item, (item) => Annotation.set(item, InFlightAnnotation, true));\n return Feed.append(feed, [item]).pipe(Effect.asVoid);\n }\n\n ack(feed: Feed.Feed, item: Message.Message | Alarm.Alarm): Effect.Effect<void, never, Database.Service> {\n Obj.update(item, (item) => Annotation.set(item, ConsumedAnnotation, true));\n return Feed.append(feed, [item]).pipe(Effect.asVoid);\n }\n\n /**\n * Schedules an alarm by appending an {@link Alarm.Alarm} record. Multiple alarms may be pending.\n */\n setAlarm(feed: Feed.Feed, props: SetAlarmProps): Effect.Effect<Alarm.Alarm, never, Database.Service> {\n const alarm = Alarm.make(props);\n return Feed.append(feed, [alarm]).pipe(Effect.as(alarm));\n }\n\n /**\n * Cancels a pending alarm (plain feed CRUD; an already-acked alarm needs no cancellation).\n */\n cancelAlarm(feed: Feed.Feed, alarm: Alarm.Alarm): Effect.Effect<void, never, Database.Service> {\n return Feed.remove(feed, [alarm]);\n }\n}\n\n/**\n * Orders feed items by the position the server assigned them. Unpositioned blocks (written locally\n * and not yet acknowledged) sort last: a message just written is the newest.\n */\nconst byFeedPosition = (a: Message.Message, b: Message.Message): number => {\n const positionA = Feed.getPosition(a);\n const positionB = Feed.getPosition(b);\n return positionA === positionB ? 0 : positionA < positionB ? -1 : 1;\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\n// @import-as-namespace\n\nimport * as Context from 'effect/Context';\nimport * as DateTime from 'effect/DateTime';\nimport * as Effect from 'effect/Effect';\nimport * as Layer from 'effect/Layer';\nimport * as Option from 'effect/Option';\nimport type * as RpcClient from 'effect/rpc/RpcClient';\nimport type * as Scope from 'effect/Scope';\n\nimport { ProcessManager } from '@dxos/compute-runtime';\nimport * as LayerSpec from '@dxos/compute/LayerSpec';\nimport * as Process from '@dxos/compute/Process';\nimport * as ServiceResolver from '@dxos/compute/ServiceResolver';\nimport { Annotation, Database, EID, Feed, Filter, Obj, type URI } from '@dxos/echo';\nimport * as EffectEx from '@dxos/effect/EffectEx';\nimport { BaseError } from '@dxos/errors';\nimport { type ContentBlock, Message } from '@dxos/types';\n\nimport * as Chat from '../types/Chat.ts';\nimport * as AiContext from './AiContext.ts';\nimport { type HarnessControlRpcs } from './harness-control.ts';\nimport { SessionStore } from './SessionStore.ts';\n\nexport interface Service {\n /** The conversation {@link AiContext.Binder} (Tier A). */\n binder: Effect.Effect<AiContext.Binder, NotSupportedError>;\n /** The {@link Chat.Chat} the conversation belongs to (Tier A). */\n chat: Effect.Effect<Chat.Chat, NotSupportedError>;\n /** The conversation message history (Tier A). */\n history: Effect.Effect<Message.Message[], NotSupportedError>;\n /** Objects bound to the conversation that match the filter (Tier A). */\n queryContext<T extends Obj.Unknown>(filter: Filter.Filter<T>): Effect.Effect<T[], NotSupportedError>;\n /** Enqueue a message into the owning host's input queue (Tier B). */\n enqueueMessage(options: EnqueueMessageOptions): Effect.Effect<void, NotSupportedError>;\n /** Schedule a self-wake on the owning host (Tier B). */\n setAlarm(options: SetAlarmOptions): Effect.Effect<void, NotSupportedError>;\n}\n\n/**\n * Provides access to the controlling harness for tools and operations that are executed by the agent.\n *\n * Replaces AiContextService and AiSessionService.\n */\nexport class HarnessService extends Context.Service<HarnessService, Service>()('@dxos/assistant/HarnessService') {}\n\n/**\n * Acess current context binder.\n */\nexport const binder: Effect.Effect<AiContext.Binder, NotSupportedError, HarnessService> = Effect.flatMap(\n HarnessService,\n (service) => service.binder,\n);\n\n/**\n * The chat the conversation belongs to.\n *\n * The agent process is bound to its chat (its spawn target), so session-scoped tools resolve the\n * conversation's plan, checklist and instructions from here rather than by looking for a Chat among\n * the bound context objects.\n */\nexport const getChat: Effect.Effect<Chat.Chat, NotSupportedError, HarnessService> = Effect.flatMap(\n HarnessService,\n (service) => service.chat,\n);\n\n/**\n * Query the context using a filter.\n */\nexport const queryContext = <T extends Obj.Unknown>(\n filter: Filter.Filter<T>,\n): Effect.Effect<T[], NotSupportedError, HarnessService> =>\n Effect.flatMap(HarnessService, (service) => service.queryContext(filter));\n\ninterface EnqueueMessageOptions {\n content: ContentBlock.Any[];\n\n // TODO(dmaretskyi): Order: 'first' | 'last'\n}\n\n/**\n * Query the session history.\n */\nexport const history: Effect.Effect<Message.Message[], NotSupportedError, HarnessService> = Effect.flatMap(\n HarnessService,\n (service) => service.history,\n);\n\n/**\n * Enqueue a message to the harness.\n */\nexport const enqueueMessage = (\n options: EnqueueMessageOptions,\n): Effect.Effect<void, NotSupportedError, HarnessService> =>\n Effect.flatMap(HarnessService, (service) => service.enqueueMessage(options));\n\ninterface SetAlarmOptions {\n at: DateTime.DateTime;\n /**\n * Message to send when the alarm fires.\n */\n message: string | null;\n}\n\nexport const setAlarm = (options: SetAlarmOptions): Effect.Effect<void, NotSupportedError, HarnessService> =>\n Effect.flatMap(HarnessService, (service) => service.setAlarm(options));\n\n/**\n * Binds skills and/or objects to the conversation context.\n */\nexport const bindContext = ({\n skills,\n objects,\n}: AiContext.BindingProps): Effect.Effect<void, NotSupportedError, HarnessService> =>\n Effect.gen(function* () {\n const sessionBinder = yield* binder;\n yield* Effect.promise(() => sessionBinder.bind({ skills, objects }));\n });\n\nexport class NotSupportedError extends BaseError.extend('NotSupportedError', 'Operation not supported') {}\n\n/**\n * Process-affinity {@link LayerSpec} that materialises {@link HarnessService} from the resolution\n * context's `conversation` DXN. Merges the former `AiContext.Service` (Tier A) with live host\n * dispatch (Tier B).\n */\nexport const layerSpec: LayerSpec.LayerSpec = LayerSpec.make(\n {\n affinity: 'process',\n requires: [Database.Service, ProcessManager.Service],\n provides: [HarnessService],\n },\n (context) =>\n Layer.effect(\n HarnessService,\n Effect.gen(function* () {\n if (!context.conversation) {\n // LayerSpec.make requires a never-failing layer (error channel = never), so typed failure\n // is not possible here. Die with ServiceNotAvailableError to signal a programming error\n // (missing 'conversation' in spawn environment) that callers cannot recover from.\n return yield* Effect.die(\n new ServiceResolver.ServiceNotAvailableError(HarnessService.key, {\n message: `Service not available: ${HarnessService.key} — process spawn is missing 'conversation' in environment (set via Operation.withInvocationOptions or ProcessManager.spawn environment)`,\n }),\n );\n }\n const conversation = context.conversation;\n const processManager = yield* ProcessManager.Service;\n const runtime = yield* Effect.context<Database.Service>();\n return yield* make({ conversation, processManager, runtime });\n }),\n ),\n);\n\ninterface MakeOptions {\n conversation: URI.URI;\n processManager: Context.Service.Shape<typeof ProcessManager.Service>;\n runtime: Context.Context<Database.Service>;\n}\n\n/**\n * Builds the {@link Service} for a conversation-scoped resolution context. Tier A resolves the\n * conversation feed / {@link AiContext.Binder} once; Tier B looks up the owning host process lazily per call\n * and dispatches over its `Handle.rpc` control surface (raising {@link NotSupportedError} when no\n * live host owns the conversation).\n */\nexport const make = ({\n conversation,\n processManager,\n runtime,\n}: MakeOptions): Effect.Effect<Service, never, Scope.Scope> =>\n Effect.gen(function* () {\n const feed = yield* Database.resolve(EID.parse(conversation), Feed.Feed).pipe(\n Effect.provide(runtime),\n Effect.orDie,\n );\n const boundBinder = yield* EffectEx.acquireReleaseResource(() => new AiContext.Binder({ feed, runtime }));\n // Cached rather than resolved up front: a harness is built for every operation the agent\n // invokes, and most never ask for the chat — resolving one here would put a lookup on the tool\n // path. The agent process is spawned against the chat, so the host is discovered by the chat's\n // URI; a feed with no chat (e.g. a bare `AiSession`) keeps the feed as its own host target.\n const chat = yield* Effect.cached(Chat.loadForFeed(feed).pipe(Effect.provide(runtime)));\n return makeService({\n feed,\n chat: chatOrFail(chat),\n runtime,\n binder: boundBinder,\n owningHost: Effect.flatMap(chat, (chat) =>\n lookupOwningHost(processManager, chat ? Obj.getURI(chat) : conversation),\n ),\n });\n });\n\ninterface FromBinderOptions {\n feed: Feed.Feed;\n runtime: Context.Context<Database.Service>;\n binder: AiContext.Binder;\n}\n\n/**\n * Builds the {@link Service} from a {@link AiContext.Binder} already opened for the conversation (e.g. the\n * agent's own turn fiber, which has no `ProcessManager.Service` in scope). Tier A is fully served;\n * Tier B raises {@link NotSupportedError} since the live-host control surface is not reachable here.\n */\nexport const fromBinder = ({ feed, runtime, binder }: FromBinderOptions): Service =>\n makeService({\n feed,\n // Resolved per call rather than up front: this is the synchronous constructor, and the chat is\n // only needed by the tools that ask for it.\n chat: chatOrFail(Chat.loadForFeed(feed).pipe(Effect.provide(runtime))),\n runtime,\n binder,\n owningHost: Effect.fail(new NotSupportedError()),\n });\n\ninterface MakeServiceOptions {\n feed: Feed.Feed;\n chat: Effect.Effect<Chat.Chat, NotSupportedError>;\n runtime: Context.Context<Database.Service>;\n binder: AiContext.Binder;\n owningHost: Effect.Effect<RpcClient.RpcClient<HarnessControlRpcs>, NotSupportedError>;\n}\n\nconst makeService = ({ feed, chat, runtime, binder, owningHost }: MakeServiceOptions): Service => ({\n binder: Effect.succeed(binder),\n chat,\n history: Effect.gen(function* () {\n const messages = yield* Feed.query(feed, Filter.type(Message.Message)).run;\n return yield* new SessionStore().reifyHistory(feed, messages);\n }).pipe(Effect.provide(runtime)),\n queryContext: <T extends Obj.Unknown>(filter: Filter.Filter<T>) =>\n Effect.sync(() => {\n const match = Filter.toPredicate(filter);\n return binder.getObjects().filter(match);\n }),\n enqueueMessage: ({ content }) => owningHost.pipe(Effect.flatMap((rpc) => rpc.enqueueMessage({ content }))),\n setAlarm: ({ at, message }) =>\n owningHost.pipe(Effect.flatMap((rpc) => rpc.setAlarm({ at: DateTime.toUtc(at), message }))),\n});\n\n/** A chat-less conversation (e.g. a bare `AiSession` feed) has no chat to hand out. */\nconst chatOrFail = (chat: Effect.Effect<Chat.Chat | undefined, never>): Effect.Effect<Chat.Chat, NotSupportedError> =>\n Effect.flatMap(chat, (chat) => (chat ? Effect.succeed(chat) : Effect.fail(new NotSupportedError())));\n\n/**\n * Resolves the live host owning `conversation` per call (so a process replacement — e.g. a model\n * switch — is never captured stale) and exposes its `HarnessControl` RPC client.\n */\nconst lookupOwningHost = (\n processManager: Context.Service.Shape<typeof ProcessManager.Service>,\n conversation: URI.URI,\n): Effect.Effect<RpcClient.RpcClient<HarnessControlRpcs>, NotSupportedError> =>\n Effect.gen(function* () {\n const processes = yield* processManager.list({ target: conversation });\n const host = processes.find(\n (process) =>\n !Process.isTerminal(process.status.state) &&\n Option.getOrElse(\n Annotation.getDictionary(process.params.annotations, Process.HarnessHostAnnotation),\n () => false,\n ),\n );\n if (!host) {\n return yield* Effect.fail(new NotSupportedError());\n }\n // The host is discovered by annotation, but the HarnessControl RPC contract is fixed at the\n // spawn site (agent-process declares it as its `rpcs`); narrow the variance-erased\n // `RpcClient<any>` to that contract so the dispatch below is typed. No typed path exists here:\n // the host handle carries `RpcClient<any>` (design spec §4.4) and the concrete definition lives\n // in `@dxos/functions-runtime`, which depends on this package — importing it would cycle.\n return host.rpc as unknown as RpcClient.RpcClient<HarnessControlRpcs>;\n });\n"],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { t as __exportAll } from "./chunk-rolldown-runtime.mjs";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import { DXN, Feed, Obj, Ref, Type } from "@dxos/echo";
|
|
4
|
+
//#region src/session/SessionLink.ts
|
|
5
|
+
var SessionLink_exports = /* @__PURE__ */ __exportAll({ SessionLink: () => SessionLink });
|
|
6
|
+
/**
|
|
7
|
+
* Feed-level record that links this session to a point in another session's history.
|
|
8
|
+
* When present in a feed, the messages from the referenced feed up to and including
|
|
9
|
+
* `messageId` are prepended to the conversation history before each turn.
|
|
10
|
+
*/
|
|
11
|
+
var SessionLink = class extends Type.makeObject(DXN.make("org.dxos.type.sessionLink", "0.1.0"))(Schema.Struct({
|
|
12
|
+
/** Reference to the feed of the source session. */
|
|
13
|
+
feedRef: Ref.Ref(Feed.Feed),
|
|
14
|
+
/** Messages from the source feed up to and including this message ID are injected. */
|
|
15
|
+
messageId: Obj.ID
|
|
16
|
+
})) {};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { SessionLink_exports as n, SessionLink as t };
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=chunk-SessionLink.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-SessionLink.mjs","names":[],"sources":["../../src/session/SessionLink.ts"],"sourcesContent":["//\n// Copyright 2025 DXOS.org\n//\n\n// @import-as-namespace\n\nimport * as Schema from 'effect/Schema';\n\nimport { DXN, Feed, Obj, Ref, Type } from '@dxos/echo';\n\n/**\n * Feed-level record that links this session to a point in another session's history.\n * When present in a feed, the messages from the referenced feed up to and including\n * `messageId` are prepended to the conversation history before each turn.\n */\nexport class SessionLink extends Type.makeObject<SessionLink>(DXN.make('org.dxos.type.sessionLink', '0.1.0'))(\n Schema.Struct({\n /** Reference to the feed of the source session. */\n feedRef: Ref.Ref(Feed.Feed),\n /** Messages from the source feed up to and including this message ID are injected. */\n messageId: Obj.ID,\n }),\n) {}\n"],"mappings":";;;;;;;;;;AAeA,IAAa,cAAb,cAAiC,KAAK,WAAwB,IAAI,KAAK,6BAA6B,OAAO,CAAC,CAAC,CAC3G,OAAO,OAAO;;CAEZ,SAAS,IAAI,IAAI,KAAK,IAAI;;CAE1B,WAAW,IAAI;AACjB,CAAC,CACH,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { t as __exportAll } from "./chunk-rolldown-runtime.mjs";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Operation from "@dxos/compute/Operation";
|
|
4
|
+
import { Database, Obj } from "@dxos/echo";
|
|
5
|
+
import { invariant } from "@dxos/invariant";
|
|
6
|
+
import { log } from "@dxos/log";
|
|
7
|
+
//#region src/session/SkillHooks.ts
|
|
8
|
+
var SkillHooks_exports = /* @__PURE__ */ __exportAll({ runHooks: () => runHooks });
|
|
9
|
+
var __dxlog_file = "/__w/dxos/dxos/packages/core/compute/assistant/src/session/SkillHooks.ts";
|
|
10
|
+
/**
|
|
11
|
+
* Runs every {@link Skill.Hook} on `skills` whose spec matches `phase`. Each hook resolves its
|
|
12
|
+
* `function` ref to a {@link Operation.PersistentOperation}, deserializes it to a definition, and
|
|
13
|
+
* invokes it with the templated `input` via `invoke`. Hooks without a function ref are skipped.
|
|
14
|
+
*
|
|
15
|
+
* Hook failures are isolated: a throwing hook logs and is swallowed so one skill's hook cannot
|
|
16
|
+
* abort the request lifecycle (or the sibling hooks).
|
|
17
|
+
*/
|
|
18
|
+
var runHooks = ({ skills, phase, invoke }) => Effect.gen(function* () {
|
|
19
|
+
for (const skill of skills) for (const hook of skill.hooks ?? []) {
|
|
20
|
+
if (hook.spec._tag !== phase) continue;
|
|
21
|
+
if (!hook.function) continue;
|
|
22
|
+
yield* Effect.gen(function* () {
|
|
23
|
+
const record = yield* Database.load(hook.function);
|
|
24
|
+
invariant(Obj.instanceOf(Operation.PersistentOperation, record), "Hook function must be an operation.", {
|
|
25
|
+
"~LogMeta": "~LogMeta",
|
|
26
|
+
F: __dxlog_file,
|
|
27
|
+
L: 61,
|
|
28
|
+
S: this,
|
|
29
|
+
A: ["Obj.instanceOf(Operation.PersistentOperation, record)", "'Hook function must be an operation.'"]
|
|
30
|
+
});
|
|
31
|
+
yield* invoke(Operation.deserialize(record), { ...hook.input }, hook);
|
|
32
|
+
}).pipe(Effect.catchCause((cause) => Effect.sync(() => log.warn("skill hook failed", {
|
|
33
|
+
phase,
|
|
34
|
+
skill: Obj.getMeta(skill).key,
|
|
35
|
+
cause
|
|
36
|
+
}, {
|
|
37
|
+
"~LogMeta": "~LogMeta",
|
|
38
|
+
F: __dxlog_file,
|
|
39
|
+
L: 70,
|
|
40
|
+
S: this
|
|
41
|
+
}))));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
//#endregion
|
|
45
|
+
export { runHooks as n, SkillHooks_exports as t };
|
|
46
|
+
|
|
47
|
+
//# sourceMappingURL=chunk-SkillHooks.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-SkillHooks.mjs","names":[],"sources":["../../src/session/SkillHooks.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\n// @import-as-namespace\n\nimport * as Effect from 'effect/Effect';\n\nimport * as Operation from '@dxos/compute/Operation';\nimport * as Skill from '@dxos/compute/Skill';\nimport { Database, Obj } from '@dxos/echo';\nimport { invariant } from '@dxos/invariant';\nimport { log } from '@dxos/log';\n\n/**\n * Tag of the lifecycle point a hook fires at, matching {@link Skill.HookSpec}.\n */\nexport type HookPhase = 'begin-request' | 'end-request';\n\n/**\n * Invokes a resolved operation with the hook's templated input. Begin and end hooks fire in\n * different contexts (the agent's turn fiber vs. a child process with Tier B reachable), so the\n * invocation strategy is supplied by the caller rather than fixed here. `R` carries the invoker's\n * own requirements (e.g. `Operation.Service`) up into {@link runHooks}.\n */\nexport type HookInvoker<R> = (\n operation: Operation.Definition.Any,\n input: Record<string, unknown>,\n hook: Skill.Hook,\n) => Effect.Effect<void, never, R>;\n\n/**\n * Runs every {@link Skill.Hook} on `skills` whose spec matches `phase`. Each hook resolves its\n * `function` ref to a {@link Operation.PersistentOperation}, deserializes it to a definition, and\n * invokes it with the templated `input` via `invoke`. Hooks without a function ref are skipped.\n *\n * Hook failures are isolated: a throwing hook logs and is swallowed so one skill's hook cannot\n * abort the request lifecycle (or the sibling hooks).\n */\nexport const runHooks = <R>({\n skills,\n phase,\n invoke,\n}: {\n skills: readonly Skill.Skill[];\n phase: HookPhase;\n invoke: HookInvoker<R>;\n}): Effect.Effect<void, never, Database.Service | R> =>\n Effect.gen(function* () {\n for (const skill of skills) {\n for (const hook of skill.hooks ?? []) {\n if (hook.spec._tag !== phase) {\n continue;\n }\n if (!hook.function) {\n continue;\n }\n\n yield* Effect.gen(function* () {\n const record = yield* Database.load(hook.function!);\n invariant(Obj.instanceOf(Operation.PersistentOperation, record), 'Hook function must be an operation.');\n const operation = Operation.deserialize(record);\n // Hooks carry no event payload (the spec is a bare tag), so the input template is passed\n // through verbatim — the hook's operation reads any conversation state it needs itself.\n yield* invoke(operation, { ...hook.input }, hook);\n }).pipe(\n Effect.catchCause((cause) =>\n // Meta key rather than `Skill.getKey`: a space-authored skill has none, and an error\n // report must not itself throw.\n Effect.sync(() => log.warn('skill hook failed', { phase, skill: Obj.getMeta(skill).key, cause })),\n ),\n );\n }\n }\n });\n"],"mappings":";;;;;;;;;;;;;;;;;AAuCA,IAAa,YAAe,EAC1B,QACA,OACA,aAMA,OAAO,IAAI,aAAa;CACtB,KAAK,MAAM,SAAS,QAClB,KAAK,MAAM,QAAQ,MAAM,SAAS,CAAC,GAAG;EACpC,IAAI,KAAK,KAAK,SAAS,OACrB;EAEF,IAAI,CAAC,KAAK,UACR;EAGF,OAAO,OAAO,IAAI,aAAa;GAC7B,MAAM,SAAS,OAAO,SAAS,KAAK,KAAK,QAAS;GAClD,UAAU,IAAI,WAAW,UAAU,qBAAqB,MAAM,GAAG,uCAAoC;IAAA,YAAA;IAAA,GAAA;IAAA,GAAA;IAAA,GAAA;IAAA,GAAA,CAAA,yDAAA,uCAAA;GAAA,CAAC;GAItG,OAAO,OAHW,UAAU,YAAY,MAG1B,GAAW,EAAE,GAAG,KAAK,MAAM,GAAG,IAAI;EAClD,CAAC,CAAC,CAAC,KACD,OAAO,YAAY,UAGjB,OAAO,WAAW,IAAI,KAAK,qBAAqB;GAAE;GAAO,OAAO,IAAI,QAAQ,KAAK,CAAC,CAAC;GAAK;EAAM,GAAA;GAAA,YAAA;GAAA,GAAA;GAAA,GAAA;GAAA,GAAA;EAAA,CAAC,CAAC,CAClG,CACF;CACF;AAEJ,CAAC"}
|