@fastagent-sh/fastagent 0.22.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import { contentText } from "@earendil-works/pi-ai";
1
2
  import * as Deferred from "effect/Deferred";
2
3
  import * as Effect from "effect/Effect";
3
4
  import * as Fiber from "effect/Fiber";
@@ -91,6 +92,28 @@ function trackExtensionTurns(session) {
91
92
  session.sendCustomMessage = (...args) => track(sendCustomMessage(...args));
92
93
  return pending;
93
94
  }
95
+ /**
96
+ * Report each user message when it is WRITTEN, which is later than pi announces it: pi awaits extension `message_end`
97
+ * handlers before it appends. Emitting from the write is what makes `user_message.entryId` readable through
98
+ * `entries()` by the time the event arrives, so a reconnecting client's backfill cannot miss a message whose event
99
+ * fired before it subscribed. pi's agent awaits each listener before the next event, so the message still reports
100
+ * before the answer to it. The in-memory store reuses one record across runs, so the release restores the method.
101
+ */
102
+ function reportUserMessages(session, report) {
103
+ const record = session.sessionManager;
104
+ const append = record.appendMessage;
105
+ return Effect.acquireRelease(Effect.sync(() => {
106
+ record.appendMessage = (message) => {
107
+ const entryId = append.call(record, message);
108
+ // The same text `entries()` publishes for this entry (session-control.ts toSessionEntry).
109
+ if (message.role === "user")
110
+ report(entryId, contentText(message.content, ""));
111
+ return entryId;
112
+ };
113
+ }), () => Effect.sync(() => {
114
+ record.appendMessage = append;
115
+ }));
116
+ }
94
117
  export function createPiAgentFromSession(options) {
95
118
  const { sessionFactory, lease = inProcessLease(), observer } = options;
96
119
  async function* turn(scope, prompt, { onCancelReady, wasCancelled }) {
@@ -217,6 +240,7 @@ export function createPiAgentFromSession(options) {
217
240
  if (OWED_BY_THE_RUN.has(error.event))
218
241
  extensionFailure ??= `${error.event} failed (${error.extensionPath}): ${error.error}`;
219
242
  })), (unsubscribe) => Effect.sync(unsubscribe));
243
+ yield* reportUserMessages(session, (entryId, text) => observe({ type: "user_message", timestamp: Date.now(), runId, data: { entryId, text } }));
220
244
  const extensionTurns = trackExtensionTurns(session);
221
245
  // Completing the gate can run waiting controls synchronously; their queue events must be observed.
222
246
  yield* Deferred.succeed(bound, session);
package/dist/session.d.ts CHANGED
@@ -280,6 +280,18 @@ export type RunSettledEvent = SessionEvent<"run_settled", {
280
280
  export type MessageStartedEvent = SessionEvent<"message_started", Record<never, never>> & {
281
281
  runId: string;
282
282
  };
283
+ /**
284
+ * A user message entered the conversation: the run's opening prompt, a steer or follow-up leaving `pending`, or one an
285
+ * extension sent. It reports after the message is recorded, so `entries()` already holds `entryId` with the same
286
+ * `text` (a slash command expanded, images not included), and before the answer to it starts. The one live event a
287
+ * backfill can be deduplicated against exactly.
288
+ */
289
+ export type UserMessageEvent = SessionEvent<"user_message", {
290
+ entryId: string;
291
+ text: string;
292
+ }> & {
293
+ runId: string;
294
+ };
283
295
  export type MessageDeltaEvent = SessionEvent<"message_delta", {
284
296
  channel: "text" | "thinking";
285
297
  delta: string;
@@ -368,4 +380,4 @@ export type ServingErrorEvent = SessionEvent<"serving_error", {
368
380
  * Every event the in-process observation plane emits today: L0, L1 `queue_changed`, and the L2 events
369
381
  * (`state_changed`, `compaction_*`, `retry_scheduled`).
370
382
  */
371
- export type KnownSessionEvent = RunStartedEvent | RunSettledEvent | MessageStartedEvent | MessageDeltaEvent | MessageFinishedEvent | ToolStartedEvent | ToolProgressEvent | ToolFinishedEvent | QueueChangedEvent | StateChangedEvent | CompactionStartedEvent | CompactionFinishedEvent | RetryScheduledEvent;
383
+ export type KnownSessionEvent = RunStartedEvent | RunSettledEvent | MessageStartedEvent | UserMessageEvent | MessageDeltaEvent | MessageFinishedEvent | ToolStartedEvent | ToolProgressEvent | ToolFinishedEvent | QueueChangedEvent | StateChangedEvent | CompactionStartedEvent | CompactionFinishedEvent | RetryScheduledEvent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastagent-sh/fastagent",
3
- "version": "0.22.0",
3
+ "version": "0.22.1",
4
4
  "description": "Vibe first. Then FastAgent: turn a local agent directory into a live service in your app, on Telegram, Slack, Feishu, or behind any channel.",
5
5
  "keywords": [
6
6
  "agent",