@finalai/final-agent-provider 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/HOSTING.md
CHANGED
|
@@ -92,7 +92,7 @@ patterns, so a definition can scope a connection whose tool list it does not kno
|
|
|
92
92
|
```ts
|
|
93
93
|
export default defineFinalConnection({
|
|
94
94
|
tools: {
|
|
95
|
-
allow: ['github.*', '
|
|
95
|
+
allow: ['github.*', 'list_tasks'],
|
|
96
96
|
deny: ['github.delete_repo'],
|
|
97
97
|
approval: { 'github.delete_*': always() },
|
|
98
98
|
},
|
|
@@ -113,7 +113,7 @@ person has connected, can name the ones the harness should list up front:
|
|
|
113
113
|
|
|
114
114
|
```ts
|
|
115
115
|
export default defineFinalConnection({
|
|
116
|
-
tools: { load: ['
|
|
116
|
+
tools: { load: ['list_tasks', 'get_task', 'create_task', 'notify_user'] },
|
|
117
117
|
});
|
|
118
118
|
```
|
|
119
119
|
|
|
@@ -157,7 +157,7 @@ tool names, so a definition can scope tools it cannot name in advance:
|
|
|
157
157
|
```ts
|
|
158
158
|
export default defineFinalConnection({
|
|
159
159
|
tools: {
|
|
160
|
-
allow: ['
|
|
160
|
+
allow: ['list_tasks', 'create_task', { readOnlyHint: true }],
|
|
161
161
|
deny: [{ destructiveHint: true, readOnlyHint: false }],
|
|
162
162
|
},
|
|
163
163
|
});
|
|
@@ -95,7 +95,13 @@ const accumulateUpdate = (state, update) => {
|
|
|
95
95
|
const block = contentBlockOf(chunk.data.content);
|
|
96
96
|
if (block == null) return null;
|
|
97
97
|
const segment = segmentFor(state, chunk.data.messageId);
|
|
98
|
-
segment.blocks.
|
|
98
|
+
const last = segment.blocks.at(-1);
|
|
99
|
+
const lastBlock = last == null ? null : contentBlockOf(last);
|
|
100
|
+
const lastText = lastBlock == null ? null : plainText(lastBlock);
|
|
101
|
+
const nextText = plainText(block);
|
|
102
|
+
if (lastText != null && nextText != null)
|
|
103
|
+
segment.blocks[segment.blocks.length - 1] = { type: "text", text: `${lastText}${nextText}` };
|
|
104
|
+
else segment.blocks.push(jsonOfContentBlock(block));
|
|
99
105
|
return segment;
|
|
100
106
|
}
|
|
101
107
|
const message = agentMessageSchema.safeParse(json);
|
|
@@ -135,17 +141,18 @@ const createProjector = (config = {}) => {
|
|
|
135
141
|
}
|
|
136
142
|
const target = state.messageId == null ? segment : state;
|
|
137
143
|
target.lastSeq = ctx.seq;
|
|
138
|
-
const content = visibleContent({ segments: target === state ? state.segments : [segment] });
|
|
139
|
-
if (content.length === 0 && target.messageId == null) {
|
|
140
|
-
ctx.state.set(state);
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
144
|
+
const content = () => visibleContent({ segments: target === state ? state.segments : [segment] });
|
|
143
145
|
if (target.messageId == null) {
|
|
146
|
+
const visible = content();
|
|
147
|
+
if (visible.length === 0) {
|
|
148
|
+
ctx.state.set(state);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
144
151
|
const message = await ctx.platform.conversations.postMessage({
|
|
145
152
|
conversationId: ctx.conversation.conversationId,
|
|
146
153
|
authorParticipantId: ctx.agentParticipant.id,
|
|
147
154
|
kind: MessageKind.Message,
|
|
148
|
-
content,
|
|
155
|
+
content: visible,
|
|
149
156
|
source: { turnId: ctx.turn.id, fromSeq: ctx.seq, toSeq: ctx.seq }
|
|
150
157
|
});
|
|
151
158
|
target.messageId = message.messageId;
|
|
@@ -156,7 +163,7 @@ const createProjector = (config = {}) => {
|
|
|
156
163
|
} else if (now() - target.lastFlushAt >= interval) {
|
|
157
164
|
await ctx.platform.conversations.updateMessage({
|
|
158
165
|
messageId: target.messageId,
|
|
159
|
-
content,
|
|
166
|
+
content: content(),
|
|
160
167
|
final: false
|
|
161
168
|
});
|
|
162
169
|
target.lastFlushAt = now();
|
|
@@ -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// 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 // RoomSurfaceDefinitionIface['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 (state.messageId == null && (segment == null || segment.messageId == null)) {\n for (const previous of state.segments) {\n if (previous === segment || !previous.dirty || previous.messageId == null) continue;\n await ctx.platform.conversations.updateMessage({\n messageId: previous.messageId,\n content: visibleContent({ segments: [previous] }),\n final: false,\n });\n previous.lastFlushAt = now();\n previous.dirty = false;\n }\n }\n if (segment == null) {\n ctx.state.set(state);\n return;\n }\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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
|
|
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 // Plain text joins the run before it as it arrives (visibleContent joins\n // them the same way), so the state and each fold stay the same size\n // however long the reply grows.\n const last = segment.blocks.at(-1);\n const lastBlock = last == null ? null : contentBlockOf(last);\n const lastText = lastBlock == null ? null : plainText(lastBlock);\n const nextText = plainText(block);\n if (lastText != null && nextText != null)\n segment.blocks[segment.blocks.length - 1] = { type: 'text', text: `${lastText}${nextText}` };\n else 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 // RoomSurfaceDefinitionIface['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 (state.messageId == null && (segment == null || segment.messageId == null)) {\n for (const previous of state.segments) {\n if (previous === segment || !previous.dirty || previous.messageId == null) continue;\n await ctx.platform.conversations.updateMessage({\n messageId: previous.messageId,\n content: visibleContent({ segments: [previous] }),\n final: false,\n });\n previous.lastFlushAt = now();\n previous.dirty = false;\n }\n }\n if (segment == null) {\n ctx.state.set(state);\n return;\n }\n const target = state.messageId == null ? segment : state;\n target.lastSeq = ctx.seq;\n // Built only when it is written: between flushes a chunk only folds.\n const content = () =>\n visibleContent({ segments: target === state ? state.segments : [segment] });\n if (target.messageId == null) {\n const visible = content();\n if (visible.length === 0) {\n ctx.state.set(state);\n return;\n }\n const message = await ctx.platform.conversations.postMessage({\n conversationId: ctx.conversation.conversationId,\n authorParticipantId: ctx.agentParticipant.id,\n kind: MessageKind.Message,\n content: visible,\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: 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
|
}
|
|
@@ -11,7 +11,7 @@ const NANOCODE_FIDELITY = {
|
|
|
11
11
|
["codex" /* Codex */]: [
|
|
12
12
|
{
|
|
13
13
|
feature: "Plan mode",
|
|
14
|
-
nanocode: "`update_plan` is always offered and shown as the session plan, whose steps are the steps of a
|
|
14
|
+
nanocode: "`update_plan` is always offered and shown as the session plan, whose steps are the steps of a Final Task when the session is its working chat.",
|
|
15
15
|
remoteOnly: "Codex's own plan and collaboration modes, as your CLI configures them.",
|
|
16
16
|
gone: {
|
|
17
17
|
what: "Plan mode (read-only planning, plan-mode questions).",
|
|
@@ -222,7 +222,7 @@ const NANOCODE_FIDELITY = {
|
|
|
222
222
|
},
|
|
223
223
|
{
|
|
224
224
|
feature: "Native tool set",
|
|
225
|
-
nanocode: "`Bash`, `BashOutput` (shell); Claude Code's own `Read`, `Edit` and `Write` (files), with its rule that `Edit` and an overwriting `Write` need the file read first; Claude Code's own `TaskCreate`, `TaskGet`, `TaskList` and `TaskUpdate`, a task list kept across invocations and shown as the session plan, whose steps are the steps of a
|
|
225
|
+
nanocode: "`Bash`, `BashOutput` (shell); Claude Code's own `Read`, `Edit` and `Write` (files), with its rule that `Edit` and an overwriting `Write` need the file read first; Claude Code's own `TaskCreate`, `TaskGet`, `TaskList` and `TaskUpdate`, a task list kept across invocations and shown as the session plan, whose steps are the steps of a Final Task when the session is its working chat; `WebSearch`, Anthropic's, paid by the session's credential; and CLAUDE.md files as project instructions, from `~/.claude` and each directory down to the working directory. Like Claude Code since 2.1.283, it searches through `Bash`: no `Glob` or `Grep`.",
|
|
226
226
|
remoteOnly: "Claude Code's full tool set on your host (`TaskStop`, `WebFetch`, `NotebookEdit`, Bash's persistent working directory and background tasks, \u2026).",
|
|
227
227
|
gone: {
|
|
228
228
|
what: "`TaskStop`, `WebFetch`, `NotebookEdit`; Bash keeping its working directory between calls.",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/harnesses/fidelity.ts"],
|
|
4
|
-
"sourcesContent": ["// Harness fidelity for the serverless nanocode harnesses per engine, what nanocode\n// keeps, what requires the user's own remote CLI, and what is unavailable, with\n// a one-line reason a user can read.\n// Measured from the code and tests named in `evidence`; FIDELITY.md renders these tables, and\n// the legacy import writes the matching one into each moved agent's review file.\n\nexport enum NanocodeEngine {\n Codex = 'codex',\n Claude = 'claude',\n}\n\nexport interface FidelityRowIface {\n feature: string;\n nanocode: string;\n remoteOnly: string;\n // Null when nothing is gone.\n gone: { what: string; reason: string } | null;\n // Where the behavior is proved (test names), or why it is read from source only.\n evidence: string;\n}\n\nconst MCP_HTTP_REASON =\n 'Serverless harnesses reach MCP servers over HTTP; stdio servers need a host.';\nconst BACKGROUND_REASON =\n 'Background commands stop when the session goes idle and parks, or when it is cancelled.';\nconst SUBAGENT_REASON = 'Serverless sessions run one agent; there is no subagent runtime.';\nconst IMAGE_REASON = 'The engines take JPEG, PNG, GIF and WebP.';\n\nexport const NANOCODE_FIDELITY = {\n [NanocodeEngine.Codex]: [\n {\n feature: 'Plan mode',\n nanocode:\n '`update_plan` is always offered and shown as the session plan, whose steps are the steps of a Goal when the session is its working chat.',\n remoteOnly: \"Codex's own plan and collaboration modes, as your CLI configures them.\",\n gone: {\n what: 'Plan mode (read-only planning, plan-mode questions).',\n reason: 'Serverless Codex has no plan mode; it shares its plan as a checklist instead.',\n },\n evidence: 'nanocode codex.test.ts \"applies a patch \u2026 and projects update_plan\", \"fidelity\"',\n },\n {\n feature: 'Hooks',\n nanocode: 'None (the engine compaction hook is refused by the host).',\n remoteOnly: 'Hooks and `notify` from your `CODEX_HOME` config.',\n gone: {\n what: 'Hooks.',\n reason: 'Hooks run commands on your machine; a serverless session has none of yours.',\n },\n evidence: 'source: nanocode host-bridge.ts beforeCompaction; runtime codex config.ts',\n },\n {\n feature: 'Slash commands',\n nanocode: 'None: a leading `/` reaches the model as text.',\n remoteOnly: 'None either: the app-server has no slash-command surface.',\n gone: {\n what: 'Slash commands.',\n reason: 'Slash commands belong to the Codex terminal; ask for the action in words.',\n },\n evidence: 'source: nanocode session.ts prompt path',\n },\n {\n feature: 'Custom subagents',\n nanocode: 'None (the host refuses engine subagent routes).',\n remoteOnly: \"Codex's multi-agent tools from your CLI.\",\n gone: { what: 'Subagents.', reason: SUBAGENT_REASON },\n evidence: 'nanocode codex.test.ts \"fidelity\" (no spawn_agent offered); host-bridge.ts',\n },\n {\n feature: 'Background tasks',\n nanocode:\n '`exec_command` returns a session id after its yield time and `write_stdin` polls it, with streamed output, while the session is live.',\n remoteOnly: 'Processes on your host, kept by the Codex app-server.',\n gone: {\n what: 'Processes outliving an idle (parked) or cancelled session.',\n reason: BACKGROUND_REASON,\n },\n evidence:\n 'nanocode codex.test.ts \"runs exec_command \u2026 under never\" (streaming); source only: tools/exec.ts (yield and polling)',\n },\n {\n feature: 'Codex rollouts and `thread/resume`',\n nanocode:\n 'The conversation persists in the Final session and continues after a reconnect; accepted turns are resubmitted.',\n remoteOnly: 'Rollout files in your `CODEX_HOME` and `thread/resume` of them.',\n gone: {\n what: 'Rollout files, and continuing an existing CLI thread.',\n reason: 'History lives in the Final session, not in Codex rollout files.',\n },\n evidence:\n 'nanocode codex.test.ts \"continues the conversation \u2026 after a takeover\", \"submits a turn \u2026 again after a takeover\"',\n },\n {\n feature: 'Session fork',\n nanocode: 'None (`supports.fork` is false).',\n remoteOnly: 'None (remote-codex does not fork either).',\n gone: { what: 'Forking a session.', reason: 'Codex sessions cannot be forked on Final.' },\n evidence: 'descriptions.test.ts; runtime codex session.ts refuses fork',\n },\n {\n feature: 'MCP servers',\n nanocode:\n \"The definition's HTTP and ACP connections as `mcp__<server>__<tool>` tools, with elicitation; tools only.\",\n remoteOnly: 'Your own `config.toml` servers, stdio included, beside the definition servers.',\n gone: { what: 'stdio servers, MCP resources and prompts.', reason: MCP_HTTP_REASON },\n evidence: 'nanocode conformance.test.ts (mcp over acp/http); codex.test.ts list_changed',\n },\n {\n feature: 'Approval policies',\n nanocode:\n \"When the definition's tool policy is silent, `untrusted` asks before `exec_command` and `apply_patch`; `on-request`, `on-failure` and `never` run without asking. With `approvalsReviewer: 'auto-review'`, a reviewer model (`codex-auto-review` on a ChatGPT plan, `gpt-6-luna` otherwise, falling back to the session's model) answers what `untrusted` would ask: low and medium risk run, a rejected action goes to the person with the reviewer's reason, and a critical one is denied with codex-rs's rejection. The reviewer sees the person's messages, earlier tool calls, the definition's instructions and AGENTS.md, and `git status` before a command that could discard work. After 3 denials in a row or 10 of the last 50, the session asks the person until they allow a call, where Codex ends the turn.\",\n remoteOnly: \"The app-server's own policies together with `sandboxMode`.\",\n gone: {\n what: 'Escalation on failure and sandbox modes.',\n reason:\n \"Commands already run in the session's remote sandbox, so there is nothing to escalate to.\",\n },\n evidence:\n 'nanocode codex.test.ts \"asks before exec_command under untrusted\u2026\", \"fidelity\" (on-request, on-failure), \"\u2026under never\"; source only: tools/approvals.ts (untrusted apply_patch)',\n },\n {\n feature: 'Images in prompts',\n nanocode:\n \"A message's JPEG, PNG, GIF and WebP images go to Codex as image input, a message sent while a turn runs joining that turn as Codex's does, as do `view_image` and MCP image results.\",\n remoteOnly: 'Prompt images, passed to Codex as image input.',\n gone: { what: 'Other image types.', reason: IMAGE_REASON },\n evidence:\n 'nanocode codex.test.ts \"fidelity\" (advertises prompt images and sends them; moves an image prompt that arrives during a turn into it)',\n },\n {\n feature: 'Compaction',\n nanocode:\n 'Automatic, managed by the engine near the context window and shown as a compaction update.',\n remoteOnly: \"Codex's own automatic compaction.\",\n gone: {\n what: 'Compacting on request.',\n reason: 'Context is compacted automatically; there is no manual compact.',\n },\n evidence:\n 'source only: the engine compaction policy; the scripted-provider tests do not reach it',\n },\n {\n feature: 'Native tool set',\n nanocode:\n \"`exec_command`, `write_stdin` (shell), `apply_patch`, `view_image` (files) and `update_plan`, on the bound sandbox; `web__run`, OpenAI's web search, paid by the session's credential; and the workspace's AGENTS.md files as project instructions, from `$CODEX_HOME` and the repository root down to the working directory.\",\n remoteOnly:\n \"Codex's full tool set on your host (code mode, image generation, its MCP and plugins).\",\n gone: {\n what: 'Every other Codex tool.',\n reason: \"Serverless Codex has a fixed tool set that runs on the session's sandbox.\",\n },\n evidence: 'nanocode codex.test.ts \"fidelity\" (tool list); descriptions.test.ts',\n },\n ],\n [NanocodeEngine.Claude]: [\n {\n feature: 'Plan mode',\n nanocode:\n '`plan` adds a plan-mode section to the system prompt and asks before `Write` and `Bash`, as `default` does.',\n remoteOnly: \"Claude Code's enforced plan mode with `ExitPlanMode`.\",\n gone: {\n what: 'Enforced read-only planning.',\n reason: 'Plan mode on serverless Claude is guidance; edits still ask for approval.',\n },\n evidence: 'nanocode claude.test.ts \"fidelity\" (plan asks for Write and Bash; the write runs)',\n },\n {\n feature: 'Hooks',\n nanocode: 'None.',\n remoteOnly: 'Your hooks, through `settingSources: user, project, local`.',\n gone: {\n what: 'Hooks.',\n reason:\n 'Hooks come from your own Claude Code settings; run Claude Code on your host to keep them.',\n },\n evidence: 'runtime claude-code.test.ts (settings sources); nanocode has no hook host',\n },\n {\n feature: 'Slash commands',\n nanocode: 'None: a leading `/` reaches the model as text.',\n remoteOnly: 'Your user and project commands, through the same settings sources.',\n gone: {\n what: 'Slash commands.',\n reason:\n 'Slash commands are a Claude Code feature; run Claude Code on your host to keep them.',\n },\n evidence: 'source: nanocode session.ts prompt path',\n },\n {\n feature: 'Custom subagents',\n nanocode: 'None (no Task tool; the host refuses engine subagent routes).',\n remoteOnly: 'Your subagents, through the settings sources.',\n gone: { what: 'Subagents.', reason: SUBAGENT_REASON },\n evidence: 'nanocode claude.test.ts \"runs Bash \u2026 asks under default\" (exact tool list)',\n },\n {\n feature: 'Background tasks',\n nanocode:\n '`Bash` returns a session id for a running command and `BashOutput` polls it, while the session is live.',\n remoteOnly: 'Background shells on your host, kept by Claude Code.',\n gone: {\n what: 'Processes outliving an idle (parked) or cancelled session.',\n reason: BACKGROUND_REASON,\n },\n evidence: 'source: nanocode tools/claude.ts; polling is not exercised by a test',\n },\n {\n feature: 'Session resume',\n nanocode:\n 'The conversation persists in the Final session; accepted queued prompts resume when processing continues.',\n remoteOnly: 'Claude Code sessions in your `~/.claude`, resumed by id.',\n gone: {\n what: 'Claude Code session files and `--resume` of them.',\n reason: 'History lives in the Final session, not in Claude Code session files.',\n },\n evidence:\n 'nanocode claude.test.ts \"resumes an accepted queued prompt after the invocation stops\"',\n },\n {\n feature: 'Claude Code session fork',\n nanocode: 'None: `session/fork` is refused.',\n remoteOnly: 'Forking into a new Claude Code session (`forkSession`).',\n gone: {\n what: 'Forking a session.',\n reason: 'Forking needs Claude Code itself; use remote-claude-code to keep it.',\n },\n evidence:\n 'nanocode claude.test.ts \"fidelity\" (no session/fork); runtime claude-code.test.ts \"forks into a new Claude Code session id\"',\n },\n {\n feature: 'MCP servers',\n nanocode:\n \"The definition's HTTP and ACP connections as `mcp__<server>__<tool>` tools, with elicitation; tools only.\",\n remoteOnly:\n 'Your own servers, stdio included, with the definition servers added as `final.<name>`.',\n gone: { what: 'stdio servers, MCP resources and prompts.', reason: MCP_HTTP_REASON },\n evidence: 'nanocode conformance.test.ts (mcp over acp/http)',\n },\n {\n feature: 'Permission modes',\n nanocode:\n \"When the definition's tool policy is silent, `default` and `plan` ask before `Write` and `Bash`; `accept-edits` asks before `Bash` only; `bypass` never asks; `auto` has a reviewer model (`claude-sonnet-5-5`, falling back to the session's model) judge every `Write` and `Bash`: low and medium risk run, a rejected action goes to the person with the reviewer's reason, and a critical one is denied with its reason. The reviewer sees the person's messages, earlier tool calls, the definition's instructions and CLAUDE.md, and `git status` before a command that could discard work. After 3 denials in a row or 20 in all, the session asks the person until they allow a call.\",\n remoteOnly: \"Claude Code's permission modes and the allow and deny rules in your settings.\",\n gone: {\n what: 'Allow and deny rules from Claude Code settings.',\n reason:\n \"Claude Code settings are not imported; define allow, ask and deny rules in the agent definition's tool policy.\",\n },\n evidence: 'nanocode claude.test.ts \"fidelity\" (all four modes)',\n },\n {\n feature: 'Images in prompts',\n nanocode:\n \"A message's JPEG, PNG, GIF and WebP images go to Claude, as do `Read` of an image and MCP image results.\",\n remoteOnly: 'Prompt images, passed to Claude Code.',\n gone: { what: 'Other image types.', reason: IMAGE_REASON },\n evidence: 'nanocode claude.test.ts \"fidelity\" (advertises prompt images and sends them)',\n },\n {\n feature: 'Compaction',\n nanocode:\n 'Automatic in the engine; client updates are emitted when the engine reports compaction.',\n remoteOnly: \"Claude Code's automatic and manual (`/compact`) compaction.\",\n gone: {\n what: 'Compacting on request.',\n reason: 'The host exposes no manual compact action.',\n },\n evidence: 'source only: the engine compaction threshold; not reached by tests',\n },\n {\n feature: 'Native tool set',\n nanocode:\n \"`Bash`, `BashOutput` (shell); Claude Code's own `Read`, `Edit` and `Write` (files), with its rule that `Edit` and an overwriting `Write` need the file read first; Claude Code's own `TaskCreate`, `TaskGet`, `TaskList` and `TaskUpdate`, a task list kept across invocations and shown as the session plan, whose steps are the steps of a Goal when the session is its working chat; `WebSearch`, Anthropic's, paid by the session's credential; and CLAUDE.md files as project instructions, from `~/.claude` and each directory down to the working directory. Like Claude Code since 2.1.283, it searches through `Bash`: no `Glob` or `Grep`.\",\n remoteOnly:\n \"Claude Code's full tool set on your host (`TaskStop`, `WebFetch`, `NotebookEdit`, Bash's persistent working directory and background tasks, \u2026).\",\n gone: {\n what: '`TaskStop`, `WebFetch`, `NotebookEdit`; Bash keeping its working directory between calls.',\n reason:\n \"Serverless Claude keeps Claude Code's core tools: it fetches pages through the shell, and each Bash command starts in the workspace.\",\n },\n evidence:\n 'nanocode claude.test.ts \"runs Bash on the shell capability and asks under default\" (exact list), \"Read, Edit and Write\", \"keeps the task list as the session plan\"',\n },\n ],\n} satisfies Record<NanocodeEngine, FidelityRowIface[]>;\n\nexport const NANOCODE_HARNESS_OF = {\n [NanocodeEngine.Codex]: 'serverless-nanocode-codex',\n [NanocodeEngine.Claude]: 'serverless-nanocode-claude',\n} satisfies Record<NanocodeEngine, string>;\n\nexport const REMOTE_HARNESS_OF = {\n [NanocodeEngine.Codex]: 'remote-codex',\n [NanocodeEngine.Claude]: 'remote-claude-code',\n} satisfies Record<NanocodeEngine, string>;\n\nconst cell = (text: string): string => text.replaceAll('|', '\\\\|').replaceAll('\\n', ' ');\n\n// One engine's table as Markdown (FIDELITY.md and the import review use it as is).\nexport const renderFidelityTable = (engine: NanocodeEngine): string =>\n [\n `| | \\`${NANOCODE_HARNESS_OF[engine]}\\` keeps | only \\`${REMOTE_HARNESS_OF[engine]}\\` keeps | gone, and why |`,\n '|---|---|---|---|',\n ...NANOCODE_FIDELITY[engine].map(\n (row) =>\n `| ${cell(row.feature)} | ${cell(row.nanocode)} | ${cell(row.remoteOnly)} | ${\n row.gone == null ? 'nothing' : cell(`${row.gone.what} ${row.gone.reason}`)\n } |`,\n ),\n ].join('\\n');\n\n// Where each row is proved.\nexport const renderFidelityEvidence = (engine: NanocodeEngine): string =>\n NANOCODE_FIDELITY[engine].map((row) => `- ${row.feature}: ${row.evidence}`).join('\\n');\n\n// The generated part of FIDELITY.md for one engine.\nexport const fidelitySection = (engine: NanocodeEngine): string =>\n `${renderFidelityTable(engine)}\\n\\nEvidence:\\n\\n${renderFidelityEvidence(engine)}`;\n"],
|
|
4
|
+
"sourcesContent": ["// Harness fidelity for the serverless nanocode harnesses per engine, what nanocode\n// keeps, what requires the user's own remote CLI, and what is unavailable, with\n// a one-line reason a user can read.\n// Measured from the code and tests named in `evidence`; FIDELITY.md renders these tables, and\n// the legacy import writes the matching one into each moved agent's review file.\n\nexport enum NanocodeEngine {\n Codex = 'codex',\n Claude = 'claude',\n}\n\nexport interface FidelityRowIface {\n feature: string;\n nanocode: string;\n remoteOnly: string;\n // Null when nothing is gone.\n gone: { what: string; reason: string } | null;\n // Where the behavior is proved (test names), or why it is read from source only.\n evidence: string;\n}\n\nconst MCP_HTTP_REASON =\n 'Serverless harnesses reach MCP servers over HTTP; stdio servers need a host.';\nconst BACKGROUND_REASON =\n 'Background commands stop when the session goes idle and parks, or when it is cancelled.';\nconst SUBAGENT_REASON = 'Serverless sessions run one agent; there is no subagent runtime.';\nconst IMAGE_REASON = 'The engines take JPEG, PNG, GIF and WebP.';\n\nexport const NANOCODE_FIDELITY = {\n [NanocodeEngine.Codex]: [\n {\n feature: 'Plan mode',\n nanocode:\n '`update_plan` is always offered and shown as the session plan, whose steps are the steps of a Final Task when the session is its working chat.',\n remoteOnly: \"Codex's own plan and collaboration modes, as your CLI configures them.\",\n gone: {\n what: 'Plan mode (read-only planning, plan-mode questions).',\n reason: 'Serverless Codex has no plan mode; it shares its plan as a checklist instead.',\n },\n evidence: 'nanocode codex.test.ts \"applies a patch \u2026 and projects update_plan\", \"fidelity\"',\n },\n {\n feature: 'Hooks',\n nanocode: 'None (the engine compaction hook is refused by the host).',\n remoteOnly: 'Hooks and `notify` from your `CODEX_HOME` config.',\n gone: {\n what: 'Hooks.',\n reason: 'Hooks run commands on your machine; a serverless session has none of yours.',\n },\n evidence: 'source: nanocode host-bridge.ts beforeCompaction; runtime codex config.ts',\n },\n {\n feature: 'Slash commands',\n nanocode: 'None: a leading `/` reaches the model as text.',\n remoteOnly: 'None either: the app-server has no slash-command surface.',\n gone: {\n what: 'Slash commands.',\n reason: 'Slash commands belong to the Codex terminal; ask for the action in words.',\n },\n evidence: 'source: nanocode session.ts prompt path',\n },\n {\n feature: 'Custom subagents',\n nanocode: 'None (the host refuses engine subagent routes).',\n remoteOnly: \"Codex's multi-agent tools from your CLI.\",\n gone: { what: 'Subagents.', reason: SUBAGENT_REASON },\n evidence: 'nanocode codex.test.ts \"fidelity\" (no spawn_agent offered); host-bridge.ts',\n },\n {\n feature: 'Background tasks',\n nanocode:\n '`exec_command` returns a session id after its yield time and `write_stdin` polls it, with streamed output, while the session is live.',\n remoteOnly: 'Processes on your host, kept by the Codex app-server.',\n gone: {\n what: 'Processes outliving an idle (parked) or cancelled session.',\n reason: BACKGROUND_REASON,\n },\n evidence:\n 'nanocode codex.test.ts \"runs exec_command \u2026 under never\" (streaming); source only: tools/exec.ts (yield and polling)',\n },\n {\n feature: 'Codex rollouts and `thread/resume`',\n nanocode:\n 'The conversation persists in the Final session and continues after a reconnect; accepted turns are resubmitted.',\n remoteOnly: 'Rollout files in your `CODEX_HOME` and `thread/resume` of them.',\n gone: {\n what: 'Rollout files, and continuing an existing CLI thread.',\n reason: 'History lives in the Final session, not in Codex rollout files.',\n },\n evidence:\n 'nanocode codex.test.ts \"continues the conversation \u2026 after a takeover\", \"submits a turn \u2026 again after a takeover\"',\n },\n {\n feature: 'Session fork',\n nanocode: 'None (`supports.fork` is false).',\n remoteOnly: 'None (remote-codex does not fork either).',\n gone: { what: 'Forking a session.', reason: 'Codex sessions cannot be forked on Final.' },\n evidence: 'descriptions.test.ts; runtime codex session.ts refuses fork',\n },\n {\n feature: 'MCP servers',\n nanocode:\n \"The definition's HTTP and ACP connections as `mcp__<server>__<tool>` tools, with elicitation; tools only.\",\n remoteOnly: 'Your own `config.toml` servers, stdio included, beside the definition servers.',\n gone: { what: 'stdio servers, MCP resources and prompts.', reason: MCP_HTTP_REASON },\n evidence: 'nanocode conformance.test.ts (mcp over acp/http); codex.test.ts list_changed',\n },\n {\n feature: 'Approval policies',\n nanocode:\n \"When the definition's tool policy is silent, `untrusted` asks before `exec_command` and `apply_patch`; `on-request`, `on-failure` and `never` run without asking. With `approvalsReviewer: 'auto-review'`, a reviewer model (`codex-auto-review` on a ChatGPT plan, `gpt-6-luna` otherwise, falling back to the session's model) answers what `untrusted` would ask: low and medium risk run, a rejected action goes to the person with the reviewer's reason, and a critical one is denied with codex-rs's rejection. The reviewer sees the person's messages, earlier tool calls, the definition's instructions and AGENTS.md, and `git status` before a command that could discard work. After 3 denials in a row or 10 of the last 50, the session asks the person until they allow a call, where Codex ends the turn.\",\n remoteOnly: \"The app-server's own policies together with `sandboxMode`.\",\n gone: {\n what: 'Escalation on failure and sandbox modes.',\n reason:\n \"Commands already run in the session's remote sandbox, so there is nothing to escalate to.\",\n },\n evidence:\n 'nanocode codex.test.ts \"asks before exec_command under untrusted\u2026\", \"fidelity\" (on-request, on-failure), \"\u2026under never\"; source only: tools/approvals.ts (untrusted apply_patch)',\n },\n {\n feature: 'Images in prompts',\n nanocode:\n \"A message's JPEG, PNG, GIF and WebP images go to Codex as image input, a message sent while a turn runs joining that turn as Codex's does, as do `view_image` and MCP image results.\",\n remoteOnly: 'Prompt images, passed to Codex as image input.',\n gone: { what: 'Other image types.', reason: IMAGE_REASON },\n evidence:\n 'nanocode codex.test.ts \"fidelity\" (advertises prompt images and sends them; moves an image prompt that arrives during a turn into it)',\n },\n {\n feature: 'Compaction',\n nanocode:\n 'Automatic, managed by the engine near the context window and shown as a compaction update.',\n remoteOnly: \"Codex's own automatic compaction.\",\n gone: {\n what: 'Compacting on request.',\n reason: 'Context is compacted automatically; there is no manual compact.',\n },\n evidence:\n 'source only: the engine compaction policy; the scripted-provider tests do not reach it',\n },\n {\n feature: 'Native tool set',\n nanocode:\n \"`exec_command`, `write_stdin` (shell), `apply_patch`, `view_image` (files) and `update_plan`, on the bound sandbox; `web__run`, OpenAI's web search, paid by the session's credential; and the workspace's AGENTS.md files as project instructions, from `$CODEX_HOME` and the repository root down to the working directory.\",\n remoteOnly:\n \"Codex's full tool set on your host (code mode, image generation, its MCP and plugins).\",\n gone: {\n what: 'Every other Codex tool.',\n reason: \"Serverless Codex has a fixed tool set that runs on the session's sandbox.\",\n },\n evidence: 'nanocode codex.test.ts \"fidelity\" (tool list); descriptions.test.ts',\n },\n ],\n [NanocodeEngine.Claude]: [\n {\n feature: 'Plan mode',\n nanocode:\n '`plan` adds a plan-mode section to the system prompt and asks before `Write` and `Bash`, as `default` does.',\n remoteOnly: \"Claude Code's enforced plan mode with `ExitPlanMode`.\",\n gone: {\n what: 'Enforced read-only planning.',\n reason: 'Plan mode on serverless Claude is guidance; edits still ask for approval.',\n },\n evidence: 'nanocode claude.test.ts \"fidelity\" (plan asks for Write and Bash; the write runs)',\n },\n {\n feature: 'Hooks',\n nanocode: 'None.',\n remoteOnly: 'Your hooks, through `settingSources: user, project, local`.',\n gone: {\n what: 'Hooks.',\n reason:\n 'Hooks come from your own Claude Code settings; run Claude Code on your host to keep them.',\n },\n evidence: 'runtime claude-code.test.ts (settings sources); nanocode has no hook host',\n },\n {\n feature: 'Slash commands',\n nanocode: 'None: a leading `/` reaches the model as text.',\n remoteOnly: 'Your user and project commands, through the same settings sources.',\n gone: {\n what: 'Slash commands.',\n reason:\n 'Slash commands are a Claude Code feature; run Claude Code on your host to keep them.',\n },\n evidence: 'source: nanocode session.ts prompt path',\n },\n {\n feature: 'Custom subagents',\n nanocode: 'None (no Task tool; the host refuses engine subagent routes).',\n remoteOnly: 'Your subagents, through the settings sources.',\n gone: { what: 'Subagents.', reason: SUBAGENT_REASON },\n evidence: 'nanocode claude.test.ts \"runs Bash \u2026 asks under default\" (exact tool list)',\n },\n {\n feature: 'Background tasks',\n nanocode:\n '`Bash` returns a session id for a running command and `BashOutput` polls it, while the session is live.',\n remoteOnly: 'Background shells on your host, kept by Claude Code.',\n gone: {\n what: 'Processes outliving an idle (parked) or cancelled session.',\n reason: BACKGROUND_REASON,\n },\n evidence: 'source: nanocode tools/claude.ts; polling is not exercised by a test',\n },\n {\n feature: 'Session resume',\n nanocode:\n 'The conversation persists in the Final session; accepted queued prompts resume when processing continues.',\n remoteOnly: 'Claude Code sessions in your `~/.claude`, resumed by id.',\n gone: {\n what: 'Claude Code session files and `--resume` of them.',\n reason: 'History lives in the Final session, not in Claude Code session files.',\n },\n evidence:\n 'nanocode claude.test.ts \"resumes an accepted queued prompt after the invocation stops\"',\n },\n {\n feature: 'Claude Code session fork',\n nanocode: 'None: `session/fork` is refused.',\n remoteOnly: 'Forking into a new Claude Code session (`forkSession`).',\n gone: {\n what: 'Forking a session.',\n reason: 'Forking needs Claude Code itself; use remote-claude-code to keep it.',\n },\n evidence:\n 'nanocode claude.test.ts \"fidelity\" (no session/fork); runtime claude-code.test.ts \"forks into a new Claude Code session id\"',\n },\n {\n feature: 'MCP servers',\n nanocode:\n \"The definition's HTTP and ACP connections as `mcp__<server>__<tool>` tools, with elicitation; tools only.\",\n remoteOnly:\n 'Your own servers, stdio included, with the definition servers added as `final.<name>`.',\n gone: { what: 'stdio servers, MCP resources and prompts.', reason: MCP_HTTP_REASON },\n evidence: 'nanocode conformance.test.ts (mcp over acp/http)',\n },\n {\n feature: 'Permission modes',\n nanocode:\n \"When the definition's tool policy is silent, `default` and `plan` ask before `Write` and `Bash`; `accept-edits` asks before `Bash` only; `bypass` never asks; `auto` has a reviewer model (`claude-sonnet-5-5`, falling back to the session's model) judge every `Write` and `Bash`: low and medium risk run, a rejected action goes to the person with the reviewer's reason, and a critical one is denied with its reason. The reviewer sees the person's messages, earlier tool calls, the definition's instructions and CLAUDE.md, and `git status` before a command that could discard work. After 3 denials in a row or 20 in all, the session asks the person until they allow a call.\",\n remoteOnly: \"Claude Code's permission modes and the allow and deny rules in your settings.\",\n gone: {\n what: 'Allow and deny rules from Claude Code settings.',\n reason:\n \"Claude Code settings are not imported; define allow, ask and deny rules in the agent definition's tool policy.\",\n },\n evidence: 'nanocode claude.test.ts \"fidelity\" (all four modes)',\n },\n {\n feature: 'Images in prompts',\n nanocode:\n \"A message's JPEG, PNG, GIF and WebP images go to Claude, as do `Read` of an image and MCP image results.\",\n remoteOnly: 'Prompt images, passed to Claude Code.',\n gone: { what: 'Other image types.', reason: IMAGE_REASON },\n evidence: 'nanocode claude.test.ts \"fidelity\" (advertises prompt images and sends them)',\n },\n {\n feature: 'Compaction',\n nanocode:\n 'Automatic in the engine; client updates are emitted when the engine reports compaction.',\n remoteOnly: \"Claude Code's automatic and manual (`/compact`) compaction.\",\n gone: {\n what: 'Compacting on request.',\n reason: 'The host exposes no manual compact action.',\n },\n evidence: 'source only: the engine compaction threshold; not reached by tests',\n },\n {\n feature: 'Native tool set',\n nanocode:\n \"`Bash`, `BashOutput` (shell); Claude Code's own `Read`, `Edit` and `Write` (files), with its rule that `Edit` and an overwriting `Write` need the file read first; Claude Code's own `TaskCreate`, `TaskGet`, `TaskList` and `TaskUpdate`, a task list kept across invocations and shown as the session plan, whose steps are the steps of a Final Task when the session is its working chat; `WebSearch`, Anthropic's, paid by the session's credential; and CLAUDE.md files as project instructions, from `~/.claude` and each directory down to the working directory. Like Claude Code since 2.1.283, it searches through `Bash`: no `Glob` or `Grep`.\",\n remoteOnly:\n \"Claude Code's full tool set on your host (`TaskStop`, `WebFetch`, `NotebookEdit`, Bash's persistent working directory and background tasks, \u2026).\",\n gone: {\n what: '`TaskStop`, `WebFetch`, `NotebookEdit`; Bash keeping its working directory between calls.',\n reason:\n \"Serverless Claude keeps Claude Code's core tools: it fetches pages through the shell, and each Bash command starts in the workspace.\",\n },\n evidence:\n 'nanocode claude.test.ts \"runs Bash on the shell capability and asks under default\" (exact list), \"Read, Edit and Write\", \"keeps the task list as the session plan\"',\n },\n ],\n} satisfies Record<NanocodeEngine, FidelityRowIface[]>;\n\nexport const NANOCODE_HARNESS_OF = {\n [NanocodeEngine.Codex]: 'serverless-nanocode-codex',\n [NanocodeEngine.Claude]: 'serverless-nanocode-claude',\n} satisfies Record<NanocodeEngine, string>;\n\nexport const REMOTE_HARNESS_OF = {\n [NanocodeEngine.Codex]: 'remote-codex',\n [NanocodeEngine.Claude]: 'remote-claude-code',\n} satisfies Record<NanocodeEngine, string>;\n\nconst cell = (text: string): string => text.replaceAll('|', '\\\\|').replaceAll('\\n', ' ');\n\n// One engine's table as Markdown (FIDELITY.md and the import review use it as is).\nexport const renderFidelityTable = (engine: NanocodeEngine): string =>\n [\n `| | \\`${NANOCODE_HARNESS_OF[engine]}\\` keeps | only \\`${REMOTE_HARNESS_OF[engine]}\\` keeps | gone, and why |`,\n '|---|---|---|---|',\n ...NANOCODE_FIDELITY[engine].map(\n (row) =>\n `| ${cell(row.feature)} | ${cell(row.nanocode)} | ${cell(row.remoteOnly)} | ${\n row.gone == null ? 'nothing' : cell(`${row.gone.what} ${row.gone.reason}`)\n } |`,\n ),\n ].join('\\n');\n\n// Where each row is proved.\nexport const renderFidelityEvidence = (engine: NanocodeEngine): string =>\n NANOCODE_FIDELITY[engine].map((row) => `- ${row.feature}: ${row.evidence}`).join('\\n');\n\n// The generated part of FIDELITY.md for one engine.\nexport const fidelitySection = (engine: NanocodeEngine): string =>\n `${renderFidelityTable(engine)}\\n\\nEvidence:\\n\\n${renderFidelityEvidence(engine)}`;\n"],
|
|
5
5
|
"mappings": "AAMO,IAAK,iBAAL,kBAAKA,oBAAL;AACL,EAAAA,gBAAA,WAAQ;AACR,EAAAA,gBAAA,YAAS;AAFC,SAAAA;AAAA,GAAA;AAeZ,MAAM,kBACJ;AACF,MAAM,oBACJ;AACF,MAAM,kBAAkB;AACxB,MAAM,eAAe;AAEd,MAAM,oBAAoB;AAAA,EAC/B,CAAC,mBAAoB,GAAG;AAAA,IACtB;AAAA,MACE,SAAS;AAAA,MACT,UACE;AAAA,MACF,YAAY;AAAA,MACZ,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,MACV;AAAA,MACA,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,MACV;AAAA,MACA,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,MACV;AAAA,MACA,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,MAAM,EAAE,MAAM,cAAc,QAAQ,gBAAgB;AAAA,MACpD,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,UACE;AAAA,MACF,YAAY;AAAA,MACZ,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,MACV;AAAA,MACA,UACE;AAAA,IACJ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,UACE;AAAA,MACF,YAAY;AAAA,MACZ,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,MACV;AAAA,MACA,UACE;AAAA,IACJ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,MAAM,EAAE,MAAM,sBAAsB,QAAQ,4CAA4C;AAAA,MACxF,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,UACE;AAAA,MACF,YAAY;AAAA,MACZ,MAAM,EAAE,MAAM,6CAA6C,QAAQ,gBAAgB;AAAA,MACnF,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,UACE;AAAA,MACF,YAAY;AAAA,MACZ,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,QACE;AAAA,MACJ;AAAA,MACA,UACE;AAAA,IACJ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,UACE;AAAA,MACF,YAAY;AAAA,MACZ,MAAM,EAAE,MAAM,sBAAsB,QAAQ,aAAa;AAAA,MACzD,UACE;AAAA,IACJ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,UACE;AAAA,MACF,YAAY;AAAA,MACZ,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,MACV;AAAA,MACA,UACE;AAAA,IACJ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,UACE;AAAA,MACF,YACE;AAAA,MACF,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,MACV;AAAA,MACA,UAAU;AAAA,IACZ;AAAA,EACF;AAAA,EACA,CAAC,qBAAqB,GAAG;AAAA,IACvB;AAAA,MACE,SAAS;AAAA,MACT,UACE;AAAA,MACF,YAAY;AAAA,MACZ,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,MACV;AAAA,MACA,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,QACE;AAAA,MACJ;AAAA,MACA,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,QACE;AAAA,MACJ;AAAA,MACA,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,MAAM,EAAE,MAAM,cAAc,QAAQ,gBAAgB;AAAA,MACpD,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,UACE;AAAA,MACF,YAAY;AAAA,MACZ,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,MACV;AAAA,MACA,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,UACE;AAAA,MACF,YAAY;AAAA,MACZ,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,MACV;AAAA,MACA,UACE;AAAA,IACJ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,MACV;AAAA,MACA,UACE;AAAA,IACJ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,UACE;AAAA,MACF,YACE;AAAA,MACF,MAAM,EAAE,MAAM,6CAA6C,QAAQ,gBAAgB;AAAA,MACnF,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,UACE;AAAA,MACF,YAAY;AAAA,MACZ,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,QACE;AAAA,MACJ;AAAA,MACA,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,UACE;AAAA,MACF,YAAY;AAAA,MACZ,MAAM,EAAE,MAAM,sBAAsB,QAAQ,aAAa;AAAA,MACzD,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,UACE;AAAA,MACF,YAAY;AAAA,MACZ,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,MACV;AAAA,MACA,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,UACE;AAAA,MACF,YACE;AAAA,MACF,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,QACE;AAAA,MACJ;AAAA,MACA,UACE;AAAA,IACJ;AAAA,EACF;AACF;AAEO,MAAM,sBAAsB;AAAA,EACjC,CAAC,mBAAoB,GAAG;AAAA,EACxB,CAAC,qBAAqB,GAAG;AAC3B;AAEO,MAAM,oBAAoB;AAAA,EAC/B,CAAC,mBAAoB,GAAG;AAAA,EACxB,CAAC,qBAAqB,GAAG;AAC3B;AAEA,MAAM,OAAO,CAAC,SAAyB,KAAK,WAAW,KAAK,KAAK,EAAE,WAAW,MAAM,GAAG;AAGhF,MAAM,sBAAsB,CAAC,WAClC;AAAA,EACE,SAAS,oBAAoB,MAAM,CAAC,qBAAqB,kBAAkB,MAAM,CAAC;AAAA,EAClF;AAAA,EACA,GAAG,kBAAkB,MAAM,EAAE;AAAA,IAC3B,CAAC,QACC,KAAK,KAAK,IAAI,OAAO,CAAC,MAAM,KAAK,IAAI,QAAQ,CAAC,MAAM,KAAK,IAAI,UAAU,CAAC,MACtE,IAAI,QAAQ,OAAO,YAAY,KAAK,GAAG,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,MAAM,EAAE,CAC3E;AAAA,EACJ;AACF,EAAE,KAAK,IAAI;AAGN,MAAM,yBAAyB,CAAC,WACrC,kBAAkB,MAAM,EAAE,IAAI,CAAC,QAAQ,KAAK,IAAI,OAAO,KAAK,IAAI,QAAQ,EAAE,EAAE,KAAK,IAAI;AAGhF,MAAM,kBAAkB,CAAC,WAC9B,GAAG,oBAAoB,MAAM,CAAC;AAAA;AAAA;AAAA;AAAA,EAAoB,uBAAuB,MAAM,CAAC;",
|
|
6
6
|
"names": ["NanocodeEngine"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@finalai/final-agent-provider",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Final provider helpers for harnesses, computer and device sandboxes, the Final room surface, and external provider channels.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -209,14 +209,14 @@
|
|
|
209
209
|
},
|
|
210
210
|
"dependencies": {
|
|
211
211
|
"@agentclientprotocol/sdk": "1.8.0",
|
|
212
|
-
"@finalai/acp-session": "0.1.
|
|
212
|
+
"@finalai/acp-session": "0.1.12",
|
|
213
213
|
"@finalai/agent-protocol": "0.8.0",
|
|
214
|
-
"@finalai/agent-provider": "0.
|
|
214
|
+
"@finalai/agent-provider": "0.9.0",
|
|
215
215
|
"@finalai/computers": "0.3.1",
|
|
216
216
|
"@finalai/websockets": "0.1.7",
|
|
217
217
|
"ws": "^8.21.3",
|
|
218
218
|
"zod": "4.6.6",
|
|
219
|
-
"@finalai/agent-definition": "0.
|
|
219
|
+
"@finalai/agent-definition": "0.8.0"
|
|
220
220
|
},
|
|
221
221
|
"engines": {
|
|
222
222
|
"node": ">=26"
|