Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
32b6944
feat(core,analytics): final-answer retry hook with real-data guard fo…
steve8708 May 6, 2026
1b28f44
feat(design): generate-design action improvements + TweaksPanel polish
steve8708 May 6, 2026
23903dd
feat(design): MultiScreenCanvas + CanvasCommentPins polish
steve8708 May 6, 2026
a7db2a2
feat(design): DesignCanvas + types polish
steve8708 May 6, 2026
c69d52e
feat(design): DesignEditor expansion
steve8708 May 6, 2026
8454eac
feat(design): design-systems lib tweaks
steve8708 May 6, 2026
cb69297
fix(design): DesignEditor minor adjustment
steve8708 May 6, 2026
31f2720
feat(design): CanvasCommentPins + DesignEditor follow-up tweaks
steve8708 May 6, 2026
788fdb2
feat(calendar): event video conferencing helper + zoom tweaks
steve8708 May 6, 2026
934f00b
feat(calendar): wire video conferencing helper into create/update eve…
steve8708 May 6, 2026
f3f6159
feat(calendar): add get-zoom-status action + view-screen + handlers w…
steve8708 May 6, 2026
aeb0384
feat(calendar): EventDetailPopover follow-up tweaks
steve8708 May 6, 2026
b04eda5
feat(calendar): CreateEventDialog + Settings page tweaks
steve8708 May 6, 2026
4a46c7a
feat(calendar): event-management SKILL + AGENTS docs + CreateEventDia…
steve8708 May 6, 2026
c3a8c08
feat(calendar): more event/dialog/settings/handler tweaks
steve8708 May 6, 2026
bb83b43
feat(core): MultiTabAssistantChat sidebar loading header
steve8708 May 6, 2026
55ebcef
feat(core,clips): builder-browser tweak + clips transcript-panel + re…
steve8708 May 6, 2026
aa11321
feat(core,clips): core-routes plugin tweak + clips regenerate-title +…
steve8708 May 6, 2026
83b1c48
feat(core,clips,mail): AssistantChat + ConnectBuilderCard + SettingsP…
steve8708 May 6, 2026
d41c30f
feat(core,dispatch,clips): ConnectBuilderCard + useBuilderStatus + ex…
steve8708 May 6, 2026
3c65ccb
chore: add changeset for dispatch + sweep AGENTS docs + clips share/e…
steve8708 May 6, 2026
6fbbee0
feat(core,clips): AgentPanel + MultiTabAssistantChat + TiptapComposer…
steve8708 May 6, 2026
3f001f9
feat(core,clips,mail): ExtensionsListPage + ensure-builder-orgs scrip…
steve8708 May 6, 2026
5c53818
feat(calendar,clips,mail): list-events google-api wiring + clips tran…
steve8708 May 6, 2026
3c429bb
feat(calendar,clips): calendar AGENTS docs + agent-chat plugin tweak;…
steve8708 May 6, 2026
27c3eb6
feat(content): get/update document + LinkHoverPreview + VisualEditor …
steve8708 May 6, 2026
0e44244
feat(content): VersionHistoryPanel polish
steve8708 May 6, 2026
8d3d8e4
feat(clips,content,mail): clips r.recordingId polish + content editor…
steve8708 May 6, 2026
40102df
feat(content): VisualEditor + CodeBlockNode + ImageBlock + global.css…
steve8708 May 6, 2026
f98bd7a
feat(core): ConnectBuilderCard polish
steve8708 May 6, 2026
fefdf6e
feat(clips,content): r.recordingId + share.shareId + p.$id route polish
steve8708 May 6, 2026
2580b77
feat(mail): ComposeModal + InlineReplyComposer + use-compose-state po…
steve8708 May 6, 2026
066ae10
feat(mail): add get/update mail-settings actions + manage-draft + han…
steve8708 May 6, 2026
3d796a3
feat(mail): SettingsPage polish
steve8708 May 6, 2026
73e1a50
feat(mail): AGENTS docs + production-system-prompt + agent-chat plugi…
steve8708 May 6, 2026
1814a02
feat(mail): email-drafts skill + AGENTS docs + navigate action + use-…
steve8708 May 6, 2026
8d6d642
feat(mail): get-mail-settings + signature module follow-ups
steve8708 May 6, 2026
1f18354
fix(lint): prettier-format 6 files flagged by CI
steve8708 May 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/real-data-final-guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@agent-native/core": patch
---

Add a template hook for retrying guarded final agent answers before they are shown.
5 changes: 5 additions & 0 deletions .changeset/sidebar-loading-header.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@agent-native/core": patch
---

Match the agent sidebar loading header height to the loaded panel header.
5 changes: 5 additions & 0 deletions packages/core/src/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ export {
type ActionEntry,
type ScriptEntry,
type ProductionAgentOptions,
type AgentLoopFinalResponseGuard,
type AgentLoopFinalResponseGuardContext,
type AgentLoopFinalResponseGuardResult,
type AgentLoopToolCallSummary,
type AgentLoopToolResultSummary,
} from "./production-agent.js";
export {
type ActionTool,
Expand Down
228 changes: 228 additions & 0 deletions packages/core/src/agent/production-agent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ import {
isPlanModeToolCallAllowed,
runAgentLoop,
type ActionEntry,
type AgentLoopFinalResponseGuardContext,
} from "./production-agent.js";
import { AgentActionStopError } from "../action.js";
import {
getRequestRunContext,
runWithRequestContext,
} from "../server/request-context.js";
import type { AgentEngine, EngineEvent } from "./engine/types.js";

function actionEntry(opts: {
Expand Down Expand Up @@ -341,6 +346,83 @@ describe("runAgentLoop", () => {
expect(maxActive).toBe(2);
});

it("exposes completed tool results on the active request run context", async () => {
let streamCalls = 0;
const engine: AgentEngine = {
name: "test",
label: "Test",
defaultModel: "test-model",
supportedModels: ["test-model"],
capabilities: {
thinking: false,
promptCaching: false,
vision: false,
computerUse: false,
parallelToolCalls: false,
},
async *stream(): AsyncIterable<EngineEvent> {
streamCalls += 1;
if (streamCalls === 1) {
yield {
type: "assistant-content",
parts: [
{
type: "tool-call" as const,
id: "query-1",
name: "query-data",
input: {},
},
{
type: "tool-call" as const,
id: "save-1",
name: "save-analysis",
input: {},
},
],
};
yield { type: "stop", reason: "tool_use" };
return;
}
yield {
type: "assistant-content",
parts: [{ type: "text" as const, text: "done" }],
};
yield { type: "stop", reason: "end_turn" };
},
};
let saveSawQueryResult = false;

await runWithRequestContext({ userEmail: "a@example.com", run: {} }, () =>
runAgentLoop({
engine,
model: "test-model",
systemPrompt: "system",
tools: [],
messages: [{ role: "user", content: [{ type: "text", text: "go" }] }],
actions: {
"query-data": {
...actionEntry({ readOnly: true }),
run: async () => ({ rows: [{ count: 3 }] }),
},
"save-analysis": {
...actionEntry({ readOnly: false }),
run: async () => {
saveSawQueryResult =
getRequestRunContext()?.toolResults?.some(
(result) => result.name === "query-data",
) === true;
return "saved";
},
},
},
send: () => {},
signal: new AbortController().signal,
}),
);

expect(saveSawQueryResult).toBe(true);
});

it("keeps reads ordered around parallel-safe mutating batches", async () => {
let streamCalls = 0;
const engine: AgentEngine = {
Expand Down Expand Up @@ -682,6 +764,152 @@ describe("runAgentLoop", () => {
});
});

it("lets a final-response guard force one corrective retry before finishing", async () => {
let streamCalls = 0;
const seenMessages: any[] = [];
const engine: AgentEngine = {
name: "test",
label: "Test",
defaultModel: "test-model",
supportedModels: ["test-model"],
capabilities: {
thinking: false,
promptCaching: false,
vision: false,
computerUse: false,
parallelToolCalls: false,
},
async *stream(opts): AsyncIterable<EngineEvent> {
streamCalls += 1;
seenMessages.push(structuredClone(opts.messages));
if (streamCalls === 1) {
yield { type: "text-delta", text: "Looks up and to the right." };
yield {
type: "assistant-content",
parts: [
{ type: "text" as const, text: "Looks up and to the right." },
],
};
yield { type: "stop", reason: "end_turn" };
return;
}
if (streamCalls === 2) {
yield {
type: "assistant-content",
parts: [
{
type: "tool-call" as const,
id: "query-1",
name: "query-data",
input: { sql: "select count(*)" },
},
],
};
yield { type: "stop", reason: "tool_use" };
return;
}
yield { type: "text-delta", text: "The real count is 3." };
yield {
type: "assistant-content",
parts: [{ type: "text" as const, text: "The real count is 3." }],
};
yield { type: "stop", reason: "end_turn" };
},
};
const events: any[] = [];
const guard = vi.fn((ctx: AgentLoopFinalResponseGuardContext) => {
const hasQuery = ctx.toolResults.some((r) => r.name === "query-data");
return hasQuery
? null
: "This answer needs a real data-source query before it can be final.";
});

await runAgentLoop({
engine,
model: "test-model",
systemPrompt: "system",
tools: [],
messages: [{ role: "user", content: [{ type: "text", text: "go" }] }],
actions: {
"query-data": {
...actionEntry({ readOnly: true }),
run: async () => ({ rows: [{ count: 3 }] }),
},
},
send: (event) => events.push(event),
signal: new AbortController().signal,
finalResponseGuard: guard,
});

expect(streamCalls).toBe(3);
expect(guard).toHaveBeenCalledTimes(2);
expect(events).toContainEqual({ type: "clear" });
expect(events).toContainEqual({
type: "tool_start",
tool: "query-data",
input: { sql: "select count(*)" },
});
expect(events).toContainEqual({
type: "text",
text: "The real count is 3.",
});
expect(events.at(-1)).toEqual({ type: "done" });
expect(JSON.stringify(seenMessages[1])).toContain(
"This answer needs a real data-source query",
);
});

it("uses the final-response guard fallback after one failed corrective retry", async () => {
let streamCalls = 0;
const engine: AgentEngine = {
name: "test",
label: "Test",
defaultModel: "test-model",
supportedModels: ["test-model"],
capabilities: {
thinking: false,
promptCaching: false,
vision: false,
computerUse: false,
parallelToolCalls: false,
},
async *stream(): AsyncIterable<EngineEvent> {
streamCalls += 1;
const text = streamCalls === 1 ? "fake answer" : "still fake";
yield { type: "text-delta", text };
yield {
type: "assistant-content",
parts: [{ type: "text" as const, text }],
};
yield { type: "stop", reason: "end_turn" };
},
};
const events: any[] = [];

await runAgentLoop({
engine,
model: "test-model",
systemPrompt: "system",
tools: [],
messages: [{ role: "user", content: [{ type: "text", text: "go" }] }],
actions: {},
send: (event) => events.push(event),
signal: new AbortController().signal,
finalResponseGuard: () => ({
retryMessage: "Query a real source before answering.",
fallbackMessage: "I stopped because no real data-source query ran.",
}),
});

expect(streamCalls).toBe(2);
expect(events.filter((event) => event.type === "clear")).toHaveLength(2);
expect(events).toContainEqual({
type: "text",
text: "I stopped because no real data-source query ran.",
});
expect(events.at(-1)).toEqual({ type: "done" });
});

it("does not retry Builder gateway timeouts inside one serverless run", async () => {
let streamCalls = 0;
const engine: AgentEngine = {
Expand Down
Loading
Loading