@finalai/final-agent-provider 0.6.0 → 0.6.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.
|
@@ -3,13 +3,19 @@ import { TurnPolicy, type JsonValue, type ProjectionContextIface } from '@finala
|
|
|
3
3
|
import * as z from 'zod';
|
|
4
4
|
export declare const DEFAULT_PROJECTION_UPDATE_INTERVAL_MS = 300;
|
|
5
5
|
export declare const projectionStateSchema: z.ZodObject<{
|
|
6
|
-
messageId: z.ZodNullable<z.ZodString
|
|
6
|
+
messageId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
7
7
|
messageSeq: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
8
|
-
fromSeq: z.ZodNullable<z.ZodNumber
|
|
9
|
-
lastSeq: z.ZodNullable<z.ZodNumber
|
|
10
|
-
lastFlushAt: z.ZodNumber
|
|
11
|
-
dirty: z.ZodBoolean
|
|
8
|
+
fromSeq: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
9
|
+
lastSeq: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
10
|
+
lastFlushAt: z.ZodDefault<z.ZodNumber>;
|
|
11
|
+
dirty: z.ZodDefault<z.ZodBoolean>;
|
|
12
12
|
segments: z.ZodArray<z.ZodObject<{
|
|
13
|
+
messageId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
14
|
+
messageSeq: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
15
|
+
fromSeq: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
16
|
+
lastSeq: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
17
|
+
lastFlushAt: z.ZodDefault<z.ZodNumber>;
|
|
18
|
+
dirty: z.ZodDefault<z.ZodBoolean>;
|
|
13
19
|
acpMessageId: z.ZodString;
|
|
14
20
|
blocks: z.ZodArray<z.ZodType<import("@finalai/agent-provider").JsonObject, unknown, z.core.$ZodTypeInternals<import("@finalai/agent-provider").JsonObject, unknown>>>;
|
|
15
21
|
}, z.core.$strip>>;
|
|
@@ -22,8 +28,8 @@ export interface ProjectorConfigIface {
|
|
|
22
28
|
}
|
|
23
29
|
export declare const emptyProjectionState: () => ProjectionStateIface;
|
|
24
30
|
export declare const projectionStateOf: (state: JsonValue | null) => ProjectionStateIface;
|
|
25
|
-
export declare const visibleContent: (state: ProjectionStateIface) => ContentBlock[];
|
|
26
|
-
export declare const accumulateUpdate: (state: ProjectionStateIface, update: SessionUpdate) =>
|
|
31
|
+
export declare const visibleContent: (state: Pick<ProjectionStateIface, "segments">) => ContentBlock[];
|
|
32
|
+
export declare const accumulateUpdate: (state: ProjectionStateIface, update: SessionUpdate) => ProjectionStateIface["segments"][number] | null;
|
|
27
33
|
export interface ProjectorIface {
|
|
28
34
|
project(update: SessionUpdate, ctx: ProjectionContextIface): Promise<void>;
|
|
29
35
|
}
|
|
@@ -8,16 +8,21 @@ import {
|
|
|
8
8
|
import * as z from "zod";
|
|
9
9
|
import { contentBlockOf, jsonOfContentBlock } from "./content.mjs";
|
|
10
10
|
const DEFAULT_PROJECTION_UPDATE_INTERVAL_MS = 300;
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
messageId: z.string().nullable(),
|
|
11
|
+
const projectedMessageSchema = z.object({
|
|
12
|
+
messageId: z.string().nullable().default(null),
|
|
14
13
|
// The reply's conversation seq, so the finished reply is read back by
|
|
15
14
|
// position however many messages arrived meanwhile.
|
|
16
15
|
messageSeq: z.number().nullable().default(null),
|
|
17
|
-
fromSeq: z.number().nullable(),
|
|
18
|
-
lastSeq: z.number().nullable(),
|
|
19
|
-
lastFlushAt: z.number(),
|
|
20
|
-
dirty: z.boolean()
|
|
16
|
+
fromSeq: z.number().nullable().default(null),
|
|
17
|
+
lastSeq: z.number().nullable().default(null),
|
|
18
|
+
lastFlushAt: z.number().default(0),
|
|
19
|
+
dirty: z.boolean().default(false)
|
|
20
|
+
});
|
|
21
|
+
const segmentSchema = projectedMessageSchema.extend({
|
|
22
|
+
acpMessageId: z.string(),
|
|
23
|
+
blocks: z.array(jsonObjectSchema)
|
|
24
|
+
});
|
|
25
|
+
const projectionStateSchema = projectedMessageSchema.extend({
|
|
21
26
|
segments: z.array(segmentSchema)
|
|
22
27
|
});
|
|
23
28
|
const agentChunkSchema = z.object({
|
|
@@ -79,7 +84,7 @@ const segmentFor = (state, acpMessageId) => {
|
|
|
79
84
|
if (existing != null) {
|
|
80
85
|
return existing;
|
|
81
86
|
}
|
|
82
|
-
const segment = { acpMessageId, blocks: [] };
|
|
87
|
+
const segment = segmentSchema.parse({ acpMessageId, blocks: [] });
|
|
83
88
|
state.segments.push(segment);
|
|
84
89
|
return segment;
|
|
85
90
|
};
|
|
@@ -88,35 +93,39 @@ const accumulateUpdate = (state, update) => {
|
|
|
88
93
|
const chunk = agentChunkSchema.safeParse(json);
|
|
89
94
|
if (chunk.success) {
|
|
90
95
|
const block = contentBlockOf(chunk.data.content);
|
|
91
|
-
if (block == null) return
|
|
92
|
-
segmentFor(state, chunk.data.messageId)
|
|
93
|
-
|
|
96
|
+
if (block == null) return null;
|
|
97
|
+
const segment = segmentFor(state, chunk.data.messageId);
|
|
98
|
+
segment.blocks.push(jsonOfContentBlock(block));
|
|
99
|
+
return segment;
|
|
94
100
|
}
|
|
95
101
|
const message = agentMessageSchema.safeParse(json);
|
|
96
102
|
if (message.success) {
|
|
97
|
-
if (message.data.content === void 0) return
|
|
103
|
+
if (message.data.content === void 0) return null;
|
|
98
104
|
const blocks = (message.data.content ?? []).flatMap((block) => {
|
|
99
105
|
const parsed = contentBlockOf(block);
|
|
100
106
|
return parsed == null ? [] : [jsonOfContentBlock(parsed)];
|
|
101
107
|
});
|
|
102
|
-
segmentFor(state, message.data.messageId)
|
|
103
|
-
|
|
108
|
+
const segment = segmentFor(state, message.data.messageId);
|
|
109
|
+
segment.blocks = blocks;
|
|
110
|
+
return segment;
|
|
104
111
|
}
|
|
105
|
-
return
|
|
112
|
+
return null;
|
|
106
113
|
};
|
|
107
114
|
const createProjector = (config = {}) => {
|
|
108
115
|
const now = config.now ?? Date.now;
|
|
109
116
|
const interval = config.updateIntervalMs ?? DEFAULT_PROJECTION_UPDATE_INTERVAL_MS;
|
|
110
117
|
const fold = async (update, ctx) => {
|
|
111
118
|
const state = projectionStateOf(ctx.state.get());
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
119
|
+
const segment = accumulateUpdate(state, update);
|
|
120
|
+
if (segment == null) return;
|
|
121
|
+
const target = state.messageId == null ? segment : state;
|
|
122
|
+
target.lastSeq = ctx.seq;
|
|
123
|
+
const content = visibleContent({ segments: target === state ? state.segments : [segment] });
|
|
124
|
+
if (content.length === 0 && target.messageId == null) {
|
|
116
125
|
ctx.state.set(state);
|
|
117
126
|
return;
|
|
118
127
|
}
|
|
119
|
-
if (
|
|
128
|
+
if (target.messageId == null) {
|
|
120
129
|
const message = await ctx.platform.conversations.postMessage({
|
|
121
130
|
conversationId: ctx.conversation.conversationId,
|
|
122
131
|
authorParticipantId: ctx.agentParticipant.id,
|
|
@@ -124,32 +133,41 @@ const createProjector = (config = {}) => {
|
|
|
124
133
|
content,
|
|
125
134
|
source: { turnId: ctx.turn.id, fromSeq: ctx.seq, toSeq: ctx.seq }
|
|
126
135
|
});
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
} else if (now() -
|
|
136
|
+
target.messageId = message.messageId;
|
|
137
|
+
target.messageSeq = message.seq;
|
|
138
|
+
target.fromSeq = ctx.seq;
|
|
139
|
+
target.lastFlushAt = now();
|
|
140
|
+
target.dirty = false;
|
|
141
|
+
} else if (now() - target.lastFlushAt >= interval) {
|
|
133
142
|
await ctx.platform.conversations.updateMessage({
|
|
134
|
-
messageId:
|
|
143
|
+
messageId: target.messageId,
|
|
135
144
|
content,
|
|
136
145
|
final: false
|
|
137
146
|
});
|
|
138
|
-
|
|
139
|
-
|
|
147
|
+
target.lastFlushAt = now();
|
|
148
|
+
target.dirty = false;
|
|
140
149
|
} else {
|
|
141
|
-
|
|
150
|
+
target.dirty = true;
|
|
142
151
|
}
|
|
143
152
|
ctx.state.set(state);
|
|
144
153
|
};
|
|
145
154
|
const finish = async (ctx) => {
|
|
146
155
|
const state = projectionStateOf(ctx.state.get());
|
|
156
|
+
if (state.messageId != null) {
|
|
157
|
+
await finishMessage(ctx, state, visibleContent(state));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
for (const segment of state.segments) {
|
|
161
|
+
await finishMessage(ctx, segment, visibleContent({ segments: [segment] }));
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const finishMessage = async (ctx, state, content) => {
|
|
147
165
|
if (state.messageId == null) {
|
|
148
166
|
return;
|
|
149
167
|
}
|
|
150
168
|
await ctx.platform.conversations.updateMessage({
|
|
151
169
|
messageId: state.messageId,
|
|
152
|
-
content
|
|
170
|
+
content,
|
|
153
171
|
final: true
|
|
154
172
|
});
|
|
155
173
|
const reply = state.messageSeq == null ? null : (await ctx.platform.conversations.recentMessages({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/channels/projection.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ContentBlock, SessionUpdate } from '@agentclientprotocol/sdk/experimental/v2';\nimport { textOfBlock } from '@finalai/acp-session';\nimport {\n jsonObjectSchema,\n MessageKind,\n turnEndOf,\n TurnPolicy,\n type JsonValue,\n type ProjectionContextIface,\n} from '@finalai/agent-provider';\nimport * as z from 'zod';\nimport { contentBlockOf, jsonOfContentBlock } from \"./content.mjs\";\n\n// agent_message_chunk accumulates into one visible message per turn\n// segment. The message is created on the first visible chunk (streaming),\n// updated at most every `updateIntervalMs`, and finalized at the idle\n// state_update that ends the turn. The fold lives in the turn state the\n// platform keeps (ctx.state). An empty turn projects nothing.\n\nexport const DEFAULT_PROJECTION_UPDATE_INTERVAL_MS = 300;\n\nconst segmentSchema = z.object({ acpMessageId: z.string(), blocks: z.array(jsonObjectSchema) });\n\nexport const projectionStateSchema = z.object({\n messageId: z.string().nullable(),\n // The reply's conversation seq, so the finished reply is read back by\n // position however many messages arrived meanwhile.\n messageSeq: z.number().nullable().default(null),\n fromSeq: z.number().nullable(),\n lastSeq: z.number().nullable(),\n lastFlushAt: z.number(),\n dirty: z.boolean(),\n segments: z.array(segmentSchema),\n});\n\nexport type ProjectionStateIface = z.infer<typeof projectionStateSchema>;\n\nexport interface ProjectorConfigIface {\n updateIntervalMs?: number;\n now?: () => number;\n // The channel's turnPolicy, for routing finished replies (default steer).\n turnPolicy?: TurnPolicy;\n}\n\nconst agentChunkSchema = z.object({\n sessionUpdate: z.literal('agent_message_chunk'),\n messageId: z.string(),\n content: jsonObjectSchema,\n});\n\nconst agentMessageSchema = z.object({\n sessionUpdate: z.literal('agent_message'),\n messageId: z.string(),\n content: z.array(jsonObjectSchema).nullish(),\n});\n\nconst updateJson = (update: SessionUpdate): JsonValue => {\n const parsed = jsonObjectSchema.safeParse(JSON.parse(JSON.stringify(update)));\n return parsed.success ? parsed.data : null;\n};\n\nexport const emptyProjectionState = (): ProjectionStateIface => ({\n messageId: null,\n messageSeq: null,\n fromSeq: null,\n lastSeq: null,\n lastFlushAt: 0,\n dirty: false,\n segments: [],\n});\n\nexport const projectionStateOf = (state: JsonValue | null): ProjectionStateIface => {\n const parsed = projectionStateSchema.safeParse(state);\n return parsed.success ? parsed.data : emptyProjectionState();\n};\n\n// Plain text (no `_meta`) of a block, the only kind consecutive chunks merge into.\nconst plainText = (block: ContentBlock | undefined): string | null =>\n block == null || block._meta != null ? null : textOfBlock(block);\n\nconst appendBlock = (blocks: ContentBlock[], block: ContentBlock, separator: string): void => {\n const lastText = plainText(blocks.at(-1));\n const nextText = plainText(block);\n if (lastText != null && nextText != null) {\n blocks[blocks.length - 1] = { type: 'text', text: `${lastText}${separator}${nextText}` };\n } else {\n blocks.push(block);\n }\n};\n\n// The visible content of the turn: segments in order of first appearance,\n// separated by a blank line between text runs.\nexport const visibleContent = (state: ProjectionStateIface): ContentBlock[] => {\n const content: ContentBlock[] = [];\n for (const segment of state.segments) {\n const blocks: ContentBlock[] = [];\n for (const block of segment.blocks) {\n const parsed = contentBlockOf(block);\n if (parsed != null) appendBlock(blocks, parsed, '');\n }\n for (const block of blocks) appendBlock(content, block, '\\n\\n');\n }\n return content.flatMap((block): ContentBlock[] => {\n const text = textOfBlock(block);\n if (text == null) return [block];\n const trimmed = text.trim();\n return trimmed.length === 0 ? [] : [{ ...block, type: 'text', text: trimmed }];\n });\n};\n\nconst segmentFor = (\n state: ProjectionStateIface,\n acpMessageId: string,\n): ProjectionStateIface['segments'][number] => {\n const existing = state.segments.find((segment) => segment.acpMessageId === acpMessageId);\n if (existing != null) {\n return existing;\n }\n const segment = { acpMessageId, blocks: [] };\n state.segments.push(segment);\n return segment;\n};\n\n// Folds one ACP update into the state. Returns false for updates that are not\n// visible output.\nexport const accumulateUpdate = (state: ProjectionStateIface, update: SessionUpdate): boolean => {\n const json = updateJson(update);\n const chunk = agentChunkSchema.safeParse(json);\n if (chunk.success) {\n const block = contentBlockOf(chunk.data.content);\n if (block == null) return false;\n segmentFor(state, chunk.data.messageId).blocks.push(jsonOfContentBlock(block));\n return true;\n }\n const message = agentMessageSchema.safeParse(json);\n if (message.success) {\n // ACP v2 upsert: a concrete array replaces the message's content; omitted leaves it.\n if (message.data.content === undefined) return false;\n const blocks = (message.data.content ?? []).flatMap((block) => {\n const parsed = contentBlockOf(block);\n return parsed == null ? [] : [jsonOfContentBlock(parsed)];\n });\n segmentFor(state, message.data.messageId).blocks = blocks;\n return true;\n }\n return false;\n};\n\ninterface ConversationProjectionContextIface extends ProjectionContextIface {\n conversation: NonNullable<ProjectionContextIface['conversation']>;\n agentParticipant: NonNullable<ProjectionContextIface['agentParticipant']>;\n}\n\nexport interface ProjectorIface {\n // ChannelDefinitionIface['project'] for the messaging channels: folds visible\n // output into the turn's message and, at the turn-ending idle state_update,\n // finalizes and routes it.\n project(update: SessionUpdate, ctx: ProjectionContextIface): Promise<void>;\n}\n\nexport const createProjector = (config: ProjectorConfigIface = {}): ProjectorIface => {\n const now = config.now ?? Date.now;\n const interval = config.updateIntervalMs ?? DEFAULT_PROJECTION_UPDATE_INTERVAL_MS;\n\n const fold = async (\n update: SessionUpdate,\n ctx: ConversationProjectionContextIface,\n ): Promise<void> => {\n const state = projectionStateOf(ctx.state.get());\n if (!accumulateUpdate(state, update)) return;\n state.lastSeq = ctx.seq;\n const content = visibleContent(state);\n if (content.length === 0) {\n ctx.state.set(state);\n return;\n }\n if (state.messageId == null) {\n const message = await ctx.platform.conversations.postMessage({\n conversationId: ctx.conversation.conversationId,\n authorParticipantId: ctx.agentParticipant.id,\n kind: MessageKind.Message,\n content,\n source: { turnId: ctx.turn.id, fromSeq: ctx.seq, toSeq: ctx.seq },\n });\n state.messageId = message.messageId;\n state.messageSeq = message.seq;\n state.fromSeq = ctx.seq;\n state.lastFlushAt = now();\n state.dirty = false;\n } else if (now() - state.lastFlushAt >= interval) {\n await ctx.platform.conversations.updateMessage({\n messageId: state.messageId,\n content,\n final: false,\n });\n state.lastFlushAt = now();\n state.dirty = false;\n } else {\n state.dirty = true;\n }\n ctx.state.set(state);\n };\n\n const finish = async (ctx: ConversationProjectionContextIface): Promise<void> => {\n const state = projectionStateOf(ctx.state.get());\n if (state.messageId == null) {\n return;\n }\n await ctx.platform.conversations.updateMessage({\n messageId: state.messageId,\n content: visibleContent(state),\n final: true,\n });\n // The finished reply is routed like any message: other agents may observe\n // it or, when mentioned within the hop limit, be prompted.\n const reply =\n state.messageSeq == null\n ? null\n : (\n await ctx.platform.conversations.recentMessages({\n conversationId: ctx.conversation.conversationId,\n beforeSeq: state.messageSeq + 1,\n limit: 1,\n })\n ).find((message) => message.messageId === state.messageId);\n if (reply == null) {\n ctx.platform.log('finished reply not found for routing', {\n messageId: state.messageId,\n seq: state.messageSeq,\n });\n }\n if (reply != null) {\n if (ctx.platform.conversations.dispatchMessage == null)\n throw new Error('channel dispatch is unavailable');\n await ctx.platform.conversations.dispatchMessage({\n conversationId: reply.conversationId,\n messageId: reply.messageId,\n });\n }\n };\n\n const project: ProjectorIface['project'] = async (update, ctx) => {\n const { conversation, agentParticipant } = ctx;\n if (conversation == null || agentParticipant == null)\n throw new Error('Final rendering requires a Final conversation');\n const context = { ...ctx, conversation, agentParticipant };\n return turnEndOf(update) == null ? fold(update, context) : finish(context);\n };\n\n return { project };\n};\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,mBAAmB;AAC5B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP,YAAY,OAAO;AACnB,SAAS,gBAAgB,0BAA0B;AAQ5C,MAAM,wCAAwC;AAErD,MAAM,
|
|
4
|
+
"sourcesContent": ["import type { ContentBlock, SessionUpdate } from '@agentclientprotocol/sdk/experimental/v2';\nimport { textOfBlock } from '@finalai/acp-session';\nimport {\n jsonObjectSchema,\n MessageKind,\n turnEndOf,\n TurnPolicy,\n type JsonValue,\n type ProjectionContextIface,\n} from '@finalai/agent-provider';\nimport * as z from 'zod';\nimport { contentBlockOf, jsonOfContentBlock } from \"./content.mjs\";\n\n// Each ACP message ID owns a visible conversation message. The message is\n// created on its first visible chunk (streaming),\n// updated at most every `updateIntervalMs`, and finalized at the idle\n// state_update that ends the turn. The fold lives in the turn state the\n// platform keeps (ctx.state). An empty turn projects nothing.\n\nexport const DEFAULT_PROJECTION_UPDATE_INTERVAL_MS = 300;\n\nconst projectedMessageSchema = z.object({\n messageId: z.string().nullable().default(null),\n // The reply's conversation seq, so the finished reply is read back by\n // position however many messages arrived meanwhile.\n messageSeq: z.number().nullable().default(null),\n fromSeq: z.number().nullable().default(null),\n lastSeq: z.number().nullable().default(null),\n lastFlushAt: z.number().default(0),\n dirty: z.boolean().default(false),\n});\n\nconst segmentSchema = projectedMessageSchema.extend({\n acpMessageId: z.string(),\n blocks: z.array(jsonObjectSchema),\n});\n\n// Root message fields are retained for turns created by the old projector.\n// Those turns finish in their existing combined row; new turns use segment rows.\nexport const projectionStateSchema = projectedMessageSchema.extend({\n segments: z.array(segmentSchema),\n});\n\nexport type ProjectionStateIface = z.infer<typeof projectionStateSchema>;\n\nexport interface ProjectorConfigIface {\n updateIntervalMs?: number;\n now?: () => number;\n // The channel's turnPolicy, for routing finished replies (default steer).\n turnPolicy?: TurnPolicy;\n}\n\nconst agentChunkSchema = z.object({\n sessionUpdate: z.literal('agent_message_chunk'),\n messageId: z.string(),\n content: jsonObjectSchema,\n});\n\nconst agentMessageSchema = z.object({\n sessionUpdate: z.literal('agent_message'),\n messageId: z.string(),\n content: z.array(jsonObjectSchema).nullish(),\n});\n\nconst updateJson = (update: SessionUpdate): JsonValue => {\n const parsed = jsonObjectSchema.safeParse(JSON.parse(JSON.stringify(update)));\n return parsed.success ? parsed.data : null;\n};\n\nexport const emptyProjectionState = (): ProjectionStateIface => ({\n messageId: null,\n messageSeq: null,\n fromSeq: null,\n lastSeq: null,\n lastFlushAt: 0,\n dirty: false,\n segments: [],\n});\n\nexport const projectionStateOf = (state: JsonValue | null): ProjectionStateIface => {\n const parsed = projectionStateSchema.safeParse(state);\n return parsed.success ? parsed.data : emptyProjectionState();\n};\n\n// Plain text (no `_meta`) of a block, the only kind consecutive chunks merge into.\nconst plainText = (block: ContentBlock | undefined): string | null =>\n block == null || block._meta != null ? null : textOfBlock(block);\n\nconst appendBlock = (blocks: ContentBlock[], block: ContentBlock, separator: string): void => {\n const lastText = plainText(blocks.at(-1));\n const nextText = plainText(block);\n if (lastText != null && nextText != null) {\n blocks[blocks.length - 1] = { type: 'text', text: `${lastText}${separator}${nextText}` };\n } else {\n blocks.push(block);\n }\n};\n\n// The visible content of the turn: segments in order of first appearance,\n// separated by a blank line between text runs.\nexport const visibleContent = (state: Pick<ProjectionStateIface, 'segments'>): ContentBlock[] => {\n const content: ContentBlock[] = [];\n for (const segment of state.segments) {\n const blocks: ContentBlock[] = [];\n for (const block of segment.blocks) {\n const parsed = contentBlockOf(block);\n if (parsed != null) appendBlock(blocks, parsed, '');\n }\n for (const block of blocks) appendBlock(content, block, '\\n\\n');\n }\n return content.flatMap((block): ContentBlock[] => {\n const text = textOfBlock(block);\n if (text == null) return [block];\n const trimmed = text.trim();\n return trimmed.length === 0 ? [] : [{ ...block, type: 'text', text: trimmed }];\n });\n};\n\nconst segmentFor = (\n state: ProjectionStateIface,\n acpMessageId: string,\n): ProjectionStateIface['segments'][number] => {\n const existing = state.segments.find((segment) => segment.acpMessageId === acpMessageId);\n if (existing != null) {\n return existing;\n }\n const segment = segmentSchema.parse({ acpMessageId, blocks: [] });\n state.segments.push(segment);\n return segment;\n};\n\n// Folds one ACP update into the state and returns the affected segment, or null\n// for updates that are not visible output.\nexport const accumulateUpdate = (\n state: ProjectionStateIface,\n update: SessionUpdate,\n): ProjectionStateIface['segments'][number] | null => {\n const json = updateJson(update);\n const chunk = agentChunkSchema.safeParse(json);\n if (chunk.success) {\n const block = contentBlockOf(chunk.data.content);\n if (block == null) return null;\n const segment = segmentFor(state, chunk.data.messageId);\n segment.blocks.push(jsonOfContentBlock(block));\n return segment;\n }\n const message = agentMessageSchema.safeParse(json);\n if (message.success) {\n // ACP v2 upsert: a concrete array replaces the message's content; omitted leaves it.\n if (message.data.content === undefined) return null;\n const blocks = (message.data.content ?? []).flatMap((block) => {\n const parsed = contentBlockOf(block);\n return parsed == null ? [] : [jsonOfContentBlock(parsed)];\n });\n const segment = segmentFor(state, message.data.messageId);\n segment.blocks = blocks;\n return segment;\n }\n return null;\n};\n\ninterface ConversationProjectionContextIface extends ProjectionContextIface {\n conversation: NonNullable<ProjectionContextIface['conversation']>;\n agentParticipant: NonNullable<ProjectionContextIface['agentParticipant']>;\n}\n\nexport interface ProjectorIface {\n // ChannelDefinitionIface['project'] for the messaging channels: folds visible\n // output into separate messages and, at the turn-ending idle state_update,\n // finalizes and routes them in order of first appearance.\n project(update: SessionUpdate, ctx: ProjectionContextIface): Promise<void>;\n}\n\nexport const createProjector = (config: ProjectorConfigIface = {}): ProjectorIface => {\n const now = config.now ?? Date.now;\n const interval = config.updateIntervalMs ?? DEFAULT_PROJECTION_UPDATE_INTERVAL_MS;\n\n const fold = async (\n update: SessionUpdate,\n ctx: ConversationProjectionContextIface,\n ): Promise<void> => {\n const state = projectionStateOf(ctx.state.get());\n const segment = accumulateUpdate(state, update);\n if (segment == null) return;\n const target = state.messageId == null ? segment : state;\n target.lastSeq = ctx.seq;\n const content = visibleContent({ segments: target === state ? state.segments : [segment] });\n if (content.length === 0 && target.messageId == null) {\n ctx.state.set(state);\n return;\n }\n if (target.messageId == null) {\n const message = await ctx.platform.conversations.postMessage({\n conversationId: ctx.conversation.conversationId,\n authorParticipantId: ctx.agentParticipant.id,\n kind: MessageKind.Message,\n content,\n source: { turnId: ctx.turn.id, fromSeq: ctx.seq, toSeq: ctx.seq },\n });\n target.messageId = message.messageId;\n target.messageSeq = message.seq;\n target.fromSeq = ctx.seq;\n target.lastFlushAt = now();\n target.dirty = false;\n } else if (now() - target.lastFlushAt >= interval) {\n await ctx.platform.conversations.updateMessage({\n messageId: target.messageId,\n content,\n final: false,\n });\n target.lastFlushAt = now();\n target.dirty = false;\n } else {\n target.dirty = true;\n }\n ctx.state.set(state);\n };\n\n const finish = async (ctx: ConversationProjectionContextIface): Promise<void> => {\n const state = projectionStateOf(ctx.state.get());\n if (state.messageId != null) {\n await finishMessage(ctx, state, visibleContent(state));\n return;\n }\n for (const segment of state.segments) {\n await finishMessage(ctx, segment, visibleContent({ segments: [segment] }));\n }\n };\n\n const finishMessage = async (\n ctx: ConversationProjectionContextIface,\n state: z.infer<typeof projectedMessageSchema>,\n content: ContentBlock[],\n ): Promise<void> => {\n if (state.messageId == null) {\n return;\n }\n await ctx.platform.conversations.updateMessage({\n messageId: state.messageId,\n content,\n final: true,\n });\n // The finished reply is routed like any message: other agents may observe\n // it or, when mentioned within the hop limit, be prompted.\n const reply =\n state.messageSeq == null\n ? null\n : (\n await ctx.platform.conversations.recentMessages({\n conversationId: ctx.conversation.conversationId,\n beforeSeq: state.messageSeq + 1,\n limit: 1,\n })\n ).find((message) => message.messageId === state.messageId);\n if (reply == null) {\n ctx.platform.log('finished reply not found for routing', {\n messageId: state.messageId,\n seq: state.messageSeq,\n });\n }\n if (reply != null) {\n if (ctx.platform.conversations.dispatchMessage == null)\n throw new Error('channel dispatch is unavailable');\n await ctx.platform.conversations.dispatchMessage({\n conversationId: reply.conversationId,\n messageId: reply.messageId,\n });\n }\n };\n\n const project: ProjectorIface['project'] = async (update, ctx) => {\n const { conversation, agentParticipant } = ctx;\n if (conversation == null || agentParticipant == null)\n throw new Error('Final rendering requires a Final conversation');\n const context = { ...ctx, conversation, agentParticipant };\n return turnEndOf(update) == null ? fold(update, context) : finish(context);\n };\n\n return { project };\n};\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["segment"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@finalai/final-agent-provider",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Final's provider package: serverless, remote and local harnesses; the computer and device sandboxes; the messaging, schedule and event channels.",
|
|
6
6
|
"license": "Apache-2.0",
|