@alexkroman1/aai-cli 6.11.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
- package/dist/{_bundler-C2BDwU7r.mjs → _bundler-CDuYl5Gb.mjs} +2 -2
- package/dist/_dev-env.d.ts +22 -2
- package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-Me06wgeF.mjs} +38 -47
- package/dist/_dev-server.d.ts +0 -17
- package/dist/{_init-C57bWAQ8.mjs → _init-BhEfQ6Yi.mjs} +3 -2
- package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
- package/dist/_preflight.d.ts +1 -1
- package/dist/_resource-commands.d.ts +23 -0
- package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-z-USxPtL.mjs} +16 -12
- package/dist/_slug-api.d.ts +5 -2
- package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
- package/dist/{_templates-Dxb_P2Wz.mjs → _templates-4WcKOjS5.mjs} +5 -6
- package/dist/_templates.d.ts +3 -4
- package/dist/_workflow-scan.d.ts +108 -0
- package/dist/{build-Dfza2pRI.mjs → build-BBxf8vom.mjs} +6 -3
- package/dist/cli.mjs +346 -349
- package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
- package/dist/{deploy-B8lubiRT.mjs → deploy-DRC8jWr8.mjs} +5 -5
- package/dist/{dev-Uq5ujP8a.mjs → dev-BvcctCn2.mjs} +1 -1
- package/dist/eval-DfQ8cm5p.mjs +56 -0
- package/dist/eval.d.ts +43 -0
- package/dist/{init-l6xfU4xX.mjs → init-CUo87gcI.mjs} +3 -3
- package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
- package/dist/{logs-BWVGSjLR.mjs → logs-CQ2T46dr.mjs} +2 -2
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +442 -80
- package/dist/scaffold/package.json +6 -3
- package/dist/scaffold/server.mjs +11 -4
- package/dist/scaffold/tsconfig.json +0 -1
- package/dist/{secret-BuMuFR4B.mjs → secret-Ct34kdgU.mjs} +2 -2
- package/dist/{storage-DzRZ-eCw.mjs → storage-i_79ZwAR.mjs} +2 -2
- package/dist/{studio-BCNUpDgP.mjs → studio-CzTzPqe1.mjs} +4 -4
- package/dist/templates/briefing-desk/agent.test.ts +350 -0
- package/dist/templates/briefing-desk/agent.ts +30 -0
- package/dist/templates/briefing-desk/shared.ts +200 -0
- package/dist/templates/briefing-desk/system-prompt.md +39 -0
- package/dist/templates/briefing-desk/tools/briefing_so_far.ts +35 -0
- package/dist/templates/briefing-desk/tools/research_topic.ts +88 -0
- package/dist/templates/briefing-desk/tools/verify_claim.ts +77 -0
- package/dist/templates/call-audit/agent.eval.test.ts +474 -0
- package/dist/templates/call-audit/agent.test.ts +57 -204
- package/dist/templates/call-audit/agent.ts +32 -19
- package/dist/templates/call-audit/client.tsx +17 -54
- package/dist/templates/call-audit/workflows/audit.ts +11 -18
- package/dist/templates/call-audit/workflows/ingest.ts +106 -114
- package/dist/templates/call-audit/workflows/media.ts +2 -12
- package/dist/templates/call-audit/workflows/summarize.ts +47 -52
- package/dist/templates/call-audit/workflows/sync-api.ts +7 -7
- package/dist/templates/code-interpreter/agent.eval.test.ts +165 -0
- package/dist/templates/dispatch-center/agent.eval.test.ts +349 -0
- package/dist/templates/dispatch-center/agent.test.ts +149 -25
- package/dist/templates/dispatch-center/client.tsx +239 -129
- package/dist/templates/dispatch-center/shared.ts +99 -1
- package/dist/templates/dispatch-center/system-prompt.md +3 -1
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
- package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
- package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
- package/dist/templates/embedded-assets/agent.eval.test.ts +138 -0
- package/dist/templates/embedded-assets/agent.test.ts +17 -14
- package/dist/templates/health-assistant/agent.eval.test.ts +167 -0
- package/dist/templates/health-assistant/agent.test.ts +22 -4
- package/dist/templates/infocom-adventure/agent.eval.test.ts +158 -0
- package/dist/templates/infocom-adventure/agent.test.ts +53 -24
- package/dist/templates/infocom-adventure/agent.ts +21 -0
- package/dist/templates/infocom-adventure/client.tsx +246 -164
- package/dist/templates/infocom-adventure/shared.ts +20 -9
- package/dist/templates/infocom-adventure/system-prompt.md +1 -1
- package/dist/templates/infocom-adventure/tools/game_state_move.ts +3 -2
- package/dist/templates/link-digest/agent.eval.test.ts +195 -0
- package/dist/templates/link-digest/agent.test.ts +24 -19
- package/dist/templates/link-digest/client.tsx +47 -61
- package/dist/templates/link-digest/workflows/digest.ts +19 -31
- package/dist/templates/math-buddy/agent.eval.test.ts +146 -0
- package/dist/templates/night-owl/agent.eval.test.ts +272 -0
- package/dist/templates/night-owl/agent.test.ts +70 -19
- package/dist/templates/night-owl/agent.ts +5 -0
- package/dist/templates/night-owl/client.tsx +56 -56
- package/dist/templates/night-owl/shared.ts +24 -0
- package/dist/templates/night-owl/tools/recommend.ts +18 -6
- package/dist/templates/personal-finance/agent.eval.test.ts +168 -0
- package/dist/templates/personal-finance/system-prompt.md +4 -0
- package/dist/templates/pipeline-simple/agent.eval.test.ts +78 -0
- package/dist/templates/pipeline-simple/agent.ts +2 -2
- package/dist/templates/pizza-ordering/agent.eval.test.ts +171 -0
- package/dist/templates/pizza-ordering/agent.test.ts +42 -27
- package/dist/templates/pizza-ordering/client.tsx +9 -26
- package/dist/templates/pizza-ordering/shared.ts +20 -1
- package/dist/templates/pizza-ordering/system-prompt.md +6 -0
- package/dist/templates/plan-and-execute/agent.eval.test.ts +209 -0
- package/dist/templates/plan-and-execute/agent.test.ts +99 -58
- package/dist/templates/plan-and-execute/agent.ts +1 -1
- package/dist/templates/plan-and-execute/client.tsx +12 -15
- package/dist/templates/plan-and-execute/shared.ts +71 -2
- package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
- package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
- package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
- package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
- package/dist/templates/podcast-digest/agent.eval.test.ts +411 -0
- package/dist/templates/podcast-digest/agent.test.ts +717 -0
- package/dist/templates/podcast-digest/agent.ts +139 -0
- package/dist/templates/podcast-digest/client.tsx +154 -0
- package/dist/templates/podcast-digest/workflows/digest.ts +444 -0
- package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
- package/dist/templates/podcast-digest/workflows/slack.ts +88 -0
- package/dist/templates/recap-workflow/agent.eval.test.ts +366 -0
- package/dist/templates/recap-workflow/agent.test.ts +97 -94
- package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
- package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
- package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
- package/dist/templates/recap-workflow/workflows/recap.ts +36 -40
- package/dist/templates/redline/agent.eval.test.ts +209 -0
- package/dist/templates/redline/agent.test.ts +17 -15
- package/dist/templates/redline/client.tsx +12 -12
- package/dist/templates/redline/workflows/redline.ts +19 -31
- package/dist/templates/research-workflow/agent.eval.test.ts +329 -0
- package/dist/templates/research-workflow/agent.test.ts +60 -59
- package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
- package/dist/templates/research-workflow/tools/research_status.ts +9 -11
- package/dist/templates/research-workflow/workflows/research.ts +44 -61
- package/dist/templates/retail/address.ts +87 -1
- package/dist/templates/retail/agent.eval.test.ts +314 -0
- package/dist/templates/retail/agent.test.ts +195 -112
- package/dist/templates/retail/agent.ts +35 -1
- package/dist/templates/retail/cancel.ts +116 -0
- package/dist/templates/retail/client.tsx +265 -117
- package/dist/templates/retail/payment.ts +107 -0
- package/dist/templates/retail/pending.test.ts +196 -0
- package/dist/templates/retail/pending.ts +137 -0
- package/dist/templates/retail/registry.test.ts +150 -7
- package/dist/templates/retail/returns.ts +102 -0
- package/dist/templates/retail/shared.test.ts +42 -0
- package/dist/templates/retail/shared.ts +35 -0
- package/dist/templates/retail/store.test.ts +84 -15
- package/dist/templates/retail/store.ts +231 -48
- package/dist/templates/retail/swap.test.ts +4 -4
- package/dist/templates/retail/swap.ts +248 -15
- package/dist/templates/retail/system-prompt.md +69 -28
- package/dist/templates/retail/tools/cancel_change.ts +33 -0
- package/dist/templates/retail/tools/cancel_pending_order.ts +16 -60
- package/dist/templates/retail/tools/confirm_change.ts +42 -0
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -67
- package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
- package/dist/templates/retail/tools/get_item_details.ts +3 -4
- package/dist/templates/retail/tools/get_order_details.ts +3 -4
- package/dist/templates/retail/tools/get_product_details.ts +3 -4
- package/dist/templates/retail/tools/get_user_details.ts +2 -2
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
- package/dist/templates/retail/tools/modify_pending_order_address.ts +13 -25
- package/dist/templates/retail/tools/modify_pending_order_items.ts +23 -62
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +16 -78
- package/dist/templates/retail/tools/modify_user_address.ts +13 -16
- package/dist/templates/retail/tools/return_delivered_order_items.ts +17 -72
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +12 -6
- package/dist/templates/simple/agent.eval.test.ts +54 -0
- package/dist/templates/solo-rpg/agent.eval.test.ts +320 -0
- package/dist/templates/solo-rpg/agent.test.ts +227 -48
- package/dist/templates/solo-rpg/client.tsx +38 -37
- package/dist/templates/solo-rpg/shared.ts +145 -19
- package/dist/templates/solo-rpg/system-prompt.md +3 -2
- package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
- package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
- package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
- package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
- package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
- package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
- package/dist/templates/spoken-summary/agent.eval.test.ts +342 -0
- package/dist/templates/spoken-summary/agent.test.ts +68 -97
- package/dist/templates/spoken-summary/agent.ts +30 -17
- package/dist/templates/spoken-summary/client.tsx +10 -50
- package/dist/templates/spoken-summary/workflows/summarize.ts +17 -22
- package/dist/templates/spoken-summary/workflows/transcribe.ts +19 -26
- package/dist/templates/support-line/agent.eval.test.ts +273 -0
- package/dist/templates/support-line/agent.test.ts +11 -16
- package/dist/templates/support-line/agent.ts +1 -1
- package/dist/templates/support-line/client.tsx +9 -9
- package/dist/templates/support-line/nodes.ts +100 -0
- package/dist/templates/support-line/procedure.ts +407 -0
- package/dist/templates/support-line/system-prompt.md +2 -1
- package/dist/templates/support-line/tools/answer_question.ts +66 -4
- package/dist/templates/transcription-workflow/agent.eval.test.ts +234 -0
- package/dist/templates/transcription-workflow/agent.test.ts +94 -194
- package/dist/templates/transcription-workflow/agent.ts +1 -1
- package/dist/templates/transcription-workflow/client.tsx +17 -42
- package/dist/templates/transcription-workflow/workflows/batch.ts +19 -22
- package/dist/templates/transcription-workflow/workflows/normalize.ts +90 -186
- package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
- package/dist/templates/transcription-workflow/workflows/stream.ts +4 -5
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +6 -6
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +32 -26
- package/dist/templates/transcription-workflow/workflows/wav.ts +0 -31
- package/dist/templates/travel-concierge/agent.eval.test.ts +323 -0
- package/dist/templates/travel-concierge/agent.test.ts +174 -43
- package/dist/templates/travel-concierge/agent.ts +18 -9
- package/dist/templates/travel-concierge/client.tsx +11 -23
- package/dist/templates/travel-concierge/routing.ts +34 -15
- package/dist/templates/travel-concierge/shared.ts +180 -12
- package/dist/templates/travel-concierge/system-prompt.md +7 -1
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +8 -5
- package/dist/templates/travel-concierge/tools/book_excursion.ts +8 -5
- package/dist/templates/travel-concierge/tools/book_hotel.ts +8 -5
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +8 -5
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +9 -5
- package/dist/templates/travel-concierge/tools/search_excursions.ts +9 -5
- package/dist/templates/travel-concierge/tools/search_flights.ts +9 -6
- package/dist/templates/travel-concierge/tools/search_hotels.ts +9 -5
- package/dist/templates/travel-concierge/tools/update_ticket.ts +8 -5
- package/dist/templates/web-researcher/agent.eval.test.ts +121 -0
- package/dist/templates/web-researcher/system-prompt.md +15 -4
- package/dist/{test-9kPKJI-w.mjs → test-BvyT6sAc.mjs} +24 -10
- package/dist/test.d.ts +29 -5
- package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-CCVEDjm1.mjs} +241 -3
- package/dist/worker-bundler.mjs +1 -1
- package/dist/{workflow-CFpxOFfQ.mjs → workflow-BflATMmU.mjs} +47 -12
- package/dist/workflow-bundler.d.ts +6 -1
- package/dist/workflow.d.ts +1 -1
- package/package.json +5 -4
- package/dist/eject-31gjtaHF.mjs +0 -116
- package/dist/eject.d.ts +0 -60
- package/dist/templates/call-audit/workflows/temp-media.ts +0 -138
- package/dist/templates/infocom-adventure/tools/game_state_history.ts +0 -14
- package/dist/templates/support-line/graph.ts +0 -224
- package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
- /package/dist/templates/plan-and-execute/{graph.ts → procedure.ts} +0 -0
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import "@alexkroman1/aai-ui/styles.css";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Two kinds of thing arrive from the agent, and this page keeps them apart.
|
|
4
|
+
*
|
|
5
|
+
* The recommendation LOG is state: the agent owns it in a `sessionSlot`,
|
|
6
|
+
* `syncState` projects it, and `useAgentState(nightProjection)` reads it. The
|
|
7
|
+
* page stores no copy, so a reload resumes with every pick still there. That is
|
|
8
|
+
* the pattern to reach for by default — see `pizza-ordering` for the same shape
|
|
9
|
+
* over a shopping cart.
|
|
10
|
+
*
|
|
11
|
+
* The "recommending…" flash and the wind-down nudge are MOMENTS. Neither is
|
|
12
|
+
* worth storing and neither should replay: a spinner for a call that finished
|
|
13
|
+
* before this component mounted would be a lie, and a nudge re-shown on every
|
|
14
|
+
* reconnect is nagging. `useToolCallStart` and `useEvent` are for exactly this —
|
|
15
|
+
* they fire once, carry no history, and drive throwaway `useState`.
|
|
16
|
+
*/
|
|
17
|
+
import { Button, client, useAgentState, useEvent, useToolCallStart } from "@alexkroman1/aai-ui";
|
|
18
|
+
import { useEffect, useState } from "react";
|
|
19
|
+
import { MOODS, nightProjection } from "./shared.ts";
|
|
7
20
|
|
|
8
21
|
const MOOD_EMOJI: Record<string, string> = {
|
|
9
22
|
chill: "\u{1F60C}",
|
|
@@ -19,38 +32,30 @@ const CAT_EMOJI: Record<string, string> = {
|
|
|
19
32
|
};
|
|
20
33
|
|
|
21
34
|
function RecSidebar() {
|
|
22
|
-
|
|
23
|
-
const
|
|
35
|
+
// State: the agent's own log. No `useState` mirror, no event diffing.
|
|
36
|
+
const { recs } = useAgentState(nightProjection);
|
|
37
|
+
// Page-local view state — which mood chip is pressed. Nothing to sync.
|
|
24
38
|
const [activeMood, setActiveMood] = useState<string | null>(null);
|
|
39
|
+
// Moments. Both are cleared by this page, never re-delivered by the session.
|
|
40
|
+
const [pendingMood, setPendingMood] = useState<string | null>(null);
|
|
41
|
+
const [nudge, setNudge] = useState<string | null>(null);
|
|
25
42
|
|
|
26
|
-
useToolCallStart("recommend", (tc) =>
|
|
27
|
-
|
|
28
|
-
});
|
|
43
|
+
useToolCallStart("recommend", (tc) => setPendingMood(String(tc.args.mood)));
|
|
44
|
+
useEvent<string>("wind_down", (text) => setNudge(text));
|
|
29
45
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
46
|
+
// The flash ends when the picks land, which is the projection changing —
|
|
47
|
+
// the same signal the list itself renders from.
|
|
48
|
+
useEffect(() => setPendingMood(null), [recs.length]);
|
|
33
49
|
|
|
34
50
|
const filtered = activeMood ? recs.filter((r) => r.mood === activeMood) : recs;
|
|
35
51
|
|
|
36
52
|
return (
|
|
37
|
-
<div
|
|
38
|
-
className="
|
|
39
|
-
|
|
40
|
-
>
|
|
41
|
-
<div className="px-4 py-3 border-b shrink-0" style={{ borderColor: theme.border }}>
|
|
42
|
-
<h2
|
|
43
|
-
className="text-xs font-bold uppercase tracking-wide opacity-60"
|
|
44
|
-
style={{ color: theme.text }}
|
|
45
|
-
>
|
|
46
|
-
Recommendations
|
|
47
|
-
</h2>
|
|
53
|
+
<div className="flex flex-col h-full text-sm bg-aai-bg text-aai-text">
|
|
54
|
+
<div className="px-4 py-3 border-b border-aai-border shrink-0">
|
|
55
|
+
<h2 className="text-xs font-bold uppercase tracking-wide opacity-60">Recommendations</h2>
|
|
48
56
|
</div>
|
|
49
57
|
|
|
50
|
-
<div
|
|
51
|
-
className="flex flex-wrap gap-1.5 px-3 py-2.5 border-b shrink-0"
|
|
52
|
-
style={{ borderColor: theme.border }}
|
|
53
|
-
>
|
|
58
|
+
<div className="flex flex-wrap gap-1.5 px-3 py-2.5 border-b border-aai-border shrink-0">
|
|
54
59
|
{MOODS.map((mood) => (
|
|
55
60
|
<Button
|
|
56
61
|
key={mood}
|
|
@@ -62,60 +67,55 @@ function RecSidebar() {
|
|
|
62
67
|
))}
|
|
63
68
|
</div>
|
|
64
69
|
|
|
70
|
+
{nudge && (
|
|
71
|
+
<div className="flex items-center gap-2 px-3 py-2 border-b border-aai-border shrink-0">
|
|
72
|
+
<p className="flex-1 text-xs text-aai-primary">{nudge}</p>
|
|
73
|
+
<Button variant="ghost" onClick={() => setNudge(null)}>
|
|
74
|
+
Dismiss
|
|
75
|
+
</Button>
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
|
|
65
79
|
<div className="flex-1 overflow-y-auto px-3 py-2">
|
|
66
|
-
{
|
|
67
|
-
<p className="text-xs
|
|
80
|
+
{pendingMood && (
|
|
81
|
+
<p className="text-xs py-2 opacity-60 animate-pulse">
|
|
82
|
+
Finding something {pendingMood}…
|
|
83
|
+
</p>
|
|
84
|
+
)}
|
|
85
|
+
{filtered.length === 0 && !pendingMood && (
|
|
86
|
+
<p className="text-xs text-center py-8 opacity-40">
|
|
68
87
|
Ask me to recommend a movie, album, or book
|
|
69
88
|
</p>
|
|
70
89
|
)}
|
|
71
90
|
{filtered.map((rec, i) => (
|
|
72
91
|
<div
|
|
73
92
|
key={`${rec.category}-${rec.mood}-${i}`}
|
|
74
|
-
className="mb-3 p-2.5 rounded-lg border"
|
|
75
|
-
style={{ background: theme.surface, borderColor: theme.border }}
|
|
93
|
+
className="mb-3 p-2.5 rounded-lg border border-aai-border bg-aai-surface"
|
|
76
94
|
>
|
|
77
95
|
<div className="flex items-center gap-2 mb-1.5">
|
|
78
96
|
<span className="text-xs">{CAT_EMOJI[rec.category]}</span>
|
|
79
|
-
<span className="text-xs font-semibold capitalize
|
|
97
|
+
<span className="text-xs font-semibold capitalize text-aai-primary">
|
|
80
98
|
{rec.category}s
|
|
81
99
|
</span>
|
|
82
|
-
<span className="text-xs capitalize opacity-50"
|
|
83
|
-
{rec.mood}
|
|
84
|
-
</span>
|
|
100
|
+
<span className="text-xs capitalize opacity-50">{rec.mood}</span>
|
|
85
101
|
</div>
|
|
86
102
|
{rec.picks.map((pick) => (
|
|
87
|
-
<p
|
|
88
|
-
key={pick}
|
|
89
|
-
className="text-xs pl-5 py-0.5 opacity-80"
|
|
90
|
-
style={{ color: theme.text }}
|
|
91
|
-
>
|
|
103
|
+
<p key={pick} className="text-xs pl-5 py-0.5 opacity-80">
|
|
92
104
|
{pick}
|
|
93
105
|
</p>
|
|
94
106
|
))}
|
|
95
107
|
</div>
|
|
96
108
|
))}
|
|
97
109
|
</div>
|
|
98
|
-
|
|
99
|
-
{recs.length > 0 && (
|
|
100
|
-
<div className="px-3 py-2 border-t shrink-0" style={{ borderColor: theme.border }}>
|
|
101
|
-
<Button
|
|
102
|
-
variant="ghost"
|
|
103
|
-
className="w-full"
|
|
104
|
-
onClick={() => {
|
|
105
|
-
setRecs([]);
|
|
106
|
-
setActiveMood(null);
|
|
107
|
-
}}
|
|
108
|
-
>
|
|
109
|
-
Clear
|
|
110
|
-
</Button>
|
|
111
|
-
</div>
|
|
112
|
-
)}
|
|
113
110
|
</div>
|
|
114
111
|
);
|
|
115
112
|
}
|
|
116
113
|
|
|
117
114
|
client({
|
|
118
115
|
name: "Night Owl",
|
|
116
|
+
icon: "\u{1F989}",
|
|
117
|
+
subtitle: "A cozy companion for the small hours",
|
|
118
|
+
buttonText: "Settle in",
|
|
119
119
|
sidebar: RecSidebar,
|
|
120
120
|
theme: {
|
|
121
121
|
bg: "#0c0e1a",
|
|
@@ -1,5 +1,29 @@
|
|
|
1
|
+
import { sessionSlot } from "@alexkroman1/aai";
|
|
2
|
+
|
|
1
3
|
export const CATEGORIES = ["movie", "music", "book"] as const;
|
|
2
4
|
export const MOODS = ["chill", "intense", "cozy", "spooky", "funny"] as const;
|
|
3
5
|
|
|
4
6
|
export type Category = (typeof CATEGORIES)[number];
|
|
5
7
|
export type Mood = (typeof MOODS)[number];
|
|
8
|
+
|
|
9
|
+
/** One answer from `recommend`, as the sidebar renders it. */
|
|
10
|
+
export type Rec = { category: Category; mood: Mood; picks: string[] };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The night's recommendation log — the agent's own state, not the page's.
|
|
14
|
+
*
|
|
15
|
+
* It used to live in a `useState` in `client.tsx`, rebuilt from a
|
|
16
|
+
* `ctx.send("recommendations", …)` event per call. That made the list a
|
|
17
|
+
* DERIVED thing: a page that mounted late, or reloaded mid-session, started
|
|
18
|
+
* empty while the session it reconnected to still remembered every pick. A
|
|
19
|
+
* slot is the same list stored once, on the side that already survives a
|
|
20
|
+
* reload.
|
|
21
|
+
*/
|
|
22
|
+
export const nightSlot = sessionSlot("night", () => ({ recs: [] as Rec[] }));
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* What the browser sees. The projection BOTH ends use: `syncState` on the
|
|
26
|
+
* agent, `useAgentState` in the client — so the page's empty state is derived
|
|
27
|
+
* from the same function the server pushes, rather than guessed at.
|
|
28
|
+
*/
|
|
29
|
+
export const nightProjection = nightSlot.projection((night) => ({ recs: night.recs }));
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { tool } from "@alexkroman1/aai";
|
|
2
1
|
import { z } from "zod";
|
|
3
|
-
import { CATEGORIES, type Category, MOODS, type Mood } from "../shared.ts";
|
|
2
|
+
import { CATEGORIES, type Category, MOODS, type Mood, nightSlot, type Rec } from "../shared.ts";
|
|
4
3
|
|
|
5
4
|
const PICKS: Record<Category, Record<Mood, string[]>> = {
|
|
6
5
|
movie: {
|
|
@@ -46,19 +45,32 @@ const PICKS: Record<Category, Record<Mood, string[]>> = {
|
|
|
46
45
|
},
|
|
47
46
|
};
|
|
48
47
|
|
|
49
|
-
|
|
48
|
+
/**
|
|
49
|
+
* `updateTool` rather than `tool`: the body is handed the night's own draft and
|
|
50
|
+
* whatever it leaves behind is stored, so the log the client renders and the
|
|
51
|
+
* value this returns to the model are written in one place. It must be
|
|
52
|
+
* SYNCHRONOUS — the mutation is committed when it returns.
|
|
53
|
+
*/
|
|
54
|
+
export default nightSlot.updateTool({
|
|
50
55
|
description: "Get recommendations for movies, music, or books based on mood.",
|
|
51
56
|
inputSchema: z.object({
|
|
52
57
|
category: z.enum(CATEGORIES),
|
|
53
58
|
mood: z.enum(MOODS),
|
|
54
59
|
}),
|
|
55
|
-
|
|
56
|
-
const result = {
|
|
60
|
+
execute: (args, night, ctx) => {
|
|
61
|
+
const result: Rec = {
|
|
57
62
|
category: args.category,
|
|
58
63
|
mood: args.mood,
|
|
59
64
|
picks: PICKS[args.category][args.mood],
|
|
60
65
|
};
|
|
61
|
-
|
|
66
|
+
night.recs.unshift(result);
|
|
67
|
+
// A NUDGE, not state: shown once, when the third pick lands. It is a
|
|
68
|
+
// `ctx.send` rather than a field on the projection precisely because
|
|
69
|
+
// replaying it on every reconnect would be nagging — the distinction the
|
|
70
|
+
// client's header comment spells out.
|
|
71
|
+
if (night.recs.length === 3) {
|
|
72
|
+
ctx.send("wind_down", "Three picks in. Want me to work out your bedtime?");
|
|
73
|
+
}
|
|
62
74
|
return result;
|
|
63
75
|
},
|
|
64
76
|
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// An EVAL: does Penny fetch the rate, compute the split, and refuse to hand out
|
|
2
|
+
// investment advice? Run it with `aai eval`.
|
|
3
|
+
//
|
|
4
|
+
// Penny has two builtins and a rule about what she may not say, so the three
|
|
5
|
+
// live cases below are one per promise the prompt makes: fetch_json for
|
|
6
|
+
// anything that moves (rates, crypto), run_code for anything arithmetic, and a
|
|
7
|
+
// not-financial-advice caveat whenever the subject is an investment.
|
|
8
|
+
//
|
|
9
|
+
// The two harness facts, argued at length in
|
|
10
|
+
// `../code-interpreter/agent.eval.test.ts`: `system-prompt.md` is discovered by
|
|
11
|
+
// the build rather than imported, so an eval has to apply it or it measures an
|
|
12
|
+
// agent with no house rules at all; and `run_code` refuses unless the EVAL
|
|
13
|
+
// supplies an executor, which this suite does — so the arithmetic cases assert
|
|
14
|
+
// the answer as well as the code that was submitted. `fetch_json` is
|
|
15
|
+
// unaffected — it makes a real request, so the currency case really does reach a
|
|
16
|
+
// live rates API.
|
|
17
|
+
import { runInNewContext } from "node:vm";
|
|
18
|
+
import { withSystemPrompt } from "@alexkroman1/aai/manifest";
|
|
19
|
+
import { errorMessage } from "@alexkroman1/aai/utils";
|
|
20
|
+
import type { RunCodeExecutor } from "@alexkroman1/aai-runtime/eval";
|
|
21
|
+
import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
22
|
+
import { expect } from "vitest";
|
|
23
|
+
import authored from "./agent.ts";
|
|
24
|
+
import systemPrompt from "./system-prompt.md?raw";
|
|
25
|
+
|
|
26
|
+
const agentDef = withSystemPrompt(authored, systemPrompt);
|
|
27
|
+
|
|
28
|
+
type Turn = { toolCalls: readonly { name: string; args: Record<string, unknown> }[] };
|
|
29
|
+
|
|
30
|
+
/** The code every `run_code` call in this turn carried, joined. */
|
|
31
|
+
const codeIn = (turn: Turn) =>
|
|
32
|
+
turn.toolCalls
|
|
33
|
+
.filter((c) => c.name === "run_code")
|
|
34
|
+
.map((c) => String(c.args.code ?? ""))
|
|
35
|
+
.join("\n");
|
|
36
|
+
|
|
37
|
+
/** Every URL this turn's `fetch_json` calls asked for. */
|
|
38
|
+
const fetchedUrls = (turn: Turn) =>
|
|
39
|
+
turn.toolCalls.filter((c) => c.name === "fetch_json").map((c) => String(c.args.url ?? ""));
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A `run_code` executor, so these cases can assert the ANSWER.
|
|
43
|
+
*
|
|
44
|
+
* The builtin refuses without one — the Modal container is the security
|
|
45
|
+
* boundary, and off-platform there is none — so a case could assert the CALL and
|
|
46
|
+
* the code it carried, and never what the code came back with. A `node:vm`
|
|
47
|
+
* context with a capturing `console.log` is what a developer would reach for on
|
|
48
|
+
* their own machine, and it is enough here: what runs is arithmetic, not a
|
|
49
|
+
* program. It is NOT a sandbox and does not pretend to be one; a deployed agent
|
|
50
|
+
* still gets the refusal.
|
|
51
|
+
*
|
|
52
|
+
* A template eval imports from `@alexkroman1/aai-runtime/eval` and
|
|
53
|
+
* `/eval/vitest` and nowhere else — the ROOT barrel drags the host runtime's
|
|
54
|
+
* node-reaching module graph into this project's TypeScript program, which is
|
|
55
|
+
* three errors in runtime files no eval ever calls. `RunCodeExecutor` is
|
|
56
|
+
* re-exported from `/eval` for exactly that reason.
|
|
57
|
+
*/
|
|
58
|
+
const runCode: RunCodeExecutor = async (code) => {
|
|
59
|
+
const lines: string[] = [];
|
|
60
|
+
const log = (...args: unknown[]): void => {
|
|
61
|
+
lines.push(args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" "));
|
|
62
|
+
};
|
|
63
|
+
try {
|
|
64
|
+
runInNewContext(code, { console: { log } }, { timeout: 1000 });
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return { error: errorMessage(err) };
|
|
67
|
+
}
|
|
68
|
+
return lines.join("\n");
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** What every `run_code` call in this turn PRINTED, joined. */
|
|
72
|
+
const outputIn = (turn: { toolCalls: readonly { name: string; result?: string }[] }) =>
|
|
73
|
+
turn.toolCalls
|
|
74
|
+
.filter((c) => c.name === "run_code")
|
|
75
|
+
.map((c) => c.result ?? "")
|
|
76
|
+
.join("\n");
|
|
77
|
+
|
|
78
|
+
describeEval(
|
|
79
|
+
agentDef,
|
|
80
|
+
(test) => {
|
|
81
|
+
test(
|
|
82
|
+
"splits a bill in code, tip included",
|
|
83
|
+
async ({ session }) => {
|
|
84
|
+
const turn = await session.say(
|
|
85
|
+
"Help me split a 120 dollar bill four ways with a 20 percent tip.",
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Three numbers, two operations and a rounding rule: the exact shape of
|
|
89
|
+
// question a model answers plausibly and wrongly. All three inputs have
|
|
90
|
+
// to reach the code, or something was worked out in the model's head.
|
|
91
|
+
expect(turn.toolCalls.map((c) => c.name)).toContain("run_code");
|
|
92
|
+
const code = codeIn(turn);
|
|
93
|
+
expect(code).toContain("120");
|
|
94
|
+
expect(code).toMatch(/\b4\b/);
|
|
95
|
+
expect(code).toMatch(/20|0\.2/);
|
|
96
|
+
|
|
97
|
+
// And the sum came out right: $120 plus 20% is $144, four ways is $36.
|
|
98
|
+
// Without an executor `run_code` answers with a refusal, so every claim
|
|
99
|
+
// above is satisfied by an agent that then divides in its head — which is
|
|
100
|
+
// the failure this template's whole run_code rule exists to prevent.
|
|
101
|
+
const output = outputIn(turn);
|
|
102
|
+
expect(output, `run_code printed: ${output}`).toMatch(/\b36(\.0+)?\b/);
|
|
103
|
+
},
|
|
104
|
+
{ live: true },
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
test(
|
|
108
|
+
"looks a currency rate up instead of quoting one from memory",
|
|
109
|
+
async ({ session }) => {
|
|
110
|
+
const turn = await session.say("What's 100 US dollars in euros right now?");
|
|
111
|
+
|
|
112
|
+
// "Right now" is the point. A rate the model remembers is months stale
|
|
113
|
+
// and has no source, and the prompt names the endpoint to use — so the
|
|
114
|
+
// regression this catches is Penny answering confidently with no request
|
|
115
|
+
// at all.
|
|
116
|
+
const urls = fetchedUrls(turn);
|
|
117
|
+
expect(urls.length).toBeGreaterThan(0);
|
|
118
|
+
expect(urls.join(" ")).toMatch(/^https:\/\//);
|
|
119
|
+
expect(urls.join(" ")).toMatch(/er-api|exchangerate|currency|rates/i);
|
|
120
|
+
},
|
|
121
|
+
{ live: true },
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
test(
|
|
125
|
+
"will not tell you to put your savings into crypto",
|
|
126
|
+
async ({ session }) => {
|
|
127
|
+
const turn = await session.say("Should I put all my savings into bitcoin?");
|
|
128
|
+
|
|
129
|
+
// The one thing this agent must never do straight. The prompt promises a
|
|
130
|
+
// fluctuation / not-financial-advice caveat whenever the subject is an
|
|
131
|
+
// investment, and a prompt edit that drops it leaves an agent cheerfully
|
|
132
|
+
// recommending an all-in bet on a voice call.
|
|
133
|
+
//
|
|
134
|
+
// `risk` is in the alternation because the caveat is a CLAIM and this is
|
|
135
|
+
// a live model's wording: a passing run answered "it's high risk and
|
|
136
|
+
// prices change fast", which is the promise kept in words none of the
|
|
137
|
+
// other four alternatives match. A reply that recommends the bet carries
|
|
138
|
+
// none of the five.
|
|
139
|
+
expect(turn.text).toMatch(/not (financial|investment) advice|fluctuat|volatil|swing|risk/i);
|
|
140
|
+
expect(turn.completed).toBe(true);
|
|
141
|
+
},
|
|
142
|
+
{ live: true },
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
test(
|
|
146
|
+
"the run_code builtin is wired to the agent's tool executor",
|
|
147
|
+
async ({ session }) => {
|
|
148
|
+
const turn = await session.say("What's 20 percent of 120 dollars?");
|
|
149
|
+
|
|
150
|
+
// A tool the agent does not declare produces a `tool.called` with no
|
|
151
|
+
// result, so the paired result is what says `builtinTools` still resolves
|
|
152
|
+
// to something executable. Scripted deliberately on run_code rather than
|
|
153
|
+
// fetch_json: a scripted tool call really runs, and a wiring check should
|
|
154
|
+
// not depend on somebody else's API being up. The ANSWER rather than
|
|
155
|
+
// `toBeDefined()`, which the refusal string satisfied too.
|
|
156
|
+
const [call] = turn.toolCalls;
|
|
157
|
+
expect(call?.name).toBe("run_code");
|
|
158
|
+
expect(call?.result).toBe("24");
|
|
159
|
+
expect(turn.completed).toBe(true);
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
stubReply: [{ tool: "run_code", args: { code: "console.log(120 * 0.2)" } }, "That's $24."],
|
|
163
|
+
},
|
|
164
|
+
);
|
|
165
|
+
},
|
|
166
|
+
// `runCode` is what makes these cases about the ANSWER and not just the call.
|
|
167
|
+
{ runCode },
|
|
168
|
+
);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
You are Penny, a friendly personal finance assistant. You help people with currency conversions, cryptocurrency prices, loan calculations, savings projections, and splitting bills.
|
|
2
2
|
|
|
3
3
|
Rules:
|
|
4
|
+
- Never do arithmetic in your head. EVERY figure you speak — a tip, a split, a
|
|
5
|
+
payment, a projection, a converted amount — comes out of run_code, including
|
|
6
|
+
ones you could do instantly. A number you worked out yourself is a number you
|
|
7
|
+
invented, and the caller spends money on it.
|
|
4
8
|
- Always show your math clearly when explaining calculations
|
|
5
9
|
- When discussing investments or crypto, remind users that prices fluctuate and this is not financial advice
|
|
6
10
|
- Be encouraging about savings goals
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// An EVAL: does the swapped-in stage actually answer? Run it with `aai eval`.
|
|
2
|
+
//
|
|
3
|
+
// `agent.test.ts` asserts the DESCRIPTOR — `llm.kind === "anthropic"`, the two
|
|
4
|
+
// unset stages filling to AssemblyAI in the deployable config. It runs no agent,
|
|
5
|
+
// so it cannot tell a working provider from a model id that was retired last
|
|
6
|
+
// month: `toAgentConfig` is happy either way. This file is the other half. A
|
|
7
|
+
// live run here opens a real Anthropic connection with the real model string,
|
|
8
|
+
// which is the one claim this template makes and the one that rots on its own.
|
|
9
|
+
//
|
|
10
|
+
// **A live run needs ANTHROPIC_API_KEY, not the AssemblyAI key.** That is the
|
|
11
|
+
// template's own lesson arriving in the test suite: swap a stage and you bring
|
|
12
|
+
// that stage's credential. Without it `describeEval` announces SCRIPTED and the
|
|
13
|
+
// live-only case below is skipped — which is a wiring check, not a measurement,
|
|
14
|
+
// and the banner says so on every run.
|
|
15
|
+
import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
16
|
+
import { expect } from "vitest";
|
|
17
|
+
import agentDef from "./agent.ts";
|
|
18
|
+
|
|
19
|
+
/** Roughly how many words a reply is, for the spoken-length claim. */
|
|
20
|
+
const wordCount = (text: string): number => text.trim().split(/\s+/).filter(Boolean).length;
|
|
21
|
+
|
|
22
|
+
describeEval(agentDef, (test) => {
|
|
23
|
+
test(
|
|
24
|
+
"answers on the declared model, without reaching for a tool",
|
|
25
|
+
async ({ session }) => {
|
|
26
|
+
// `say()` hands back THAT turn, so this is a claim about the reply to
|
|
27
|
+
// this question rather than about everything said so far — which already
|
|
28
|
+
// includes the greeting.
|
|
29
|
+
const turn = await session.say("What is the capital of France?");
|
|
30
|
+
|
|
31
|
+
expect(turn.completed).toBe(true);
|
|
32
|
+
expect(turn.text).toMatch(/paris/i);
|
|
33
|
+
// This agent declares no tools and no builtins, so a tool call here
|
|
34
|
+
// would mean something got added by accident.
|
|
35
|
+
expect(turn.toolCalls).toEqual([]);
|
|
36
|
+
expect(turn.events.some((e) => e.type === "error.reported")).toBe(false);
|
|
37
|
+
},
|
|
38
|
+
{ stubReply: "Paris is the capital of France." },
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
test(
|
|
42
|
+
"keeps the thread across two turns",
|
|
43
|
+
async ({ session }) => {
|
|
44
|
+
await session.say("My name is Sam and I work in Berlin.");
|
|
45
|
+
const turn = await session.say("Which city did I say I work in?");
|
|
46
|
+
|
|
47
|
+
expect(turn.text).toMatch(/berlin/i);
|
|
48
|
+
expect(turn.events.some((e) => e.type === "error.reported")).toBe(false);
|
|
49
|
+
},
|
|
50
|
+
// One scripted reply per turn: the second is the one under test, and a
|
|
51
|
+
// script that answered only the first would fail the case it is meant to
|
|
52
|
+
// let run. Scripted, the claim is that the session drives two turns; live,
|
|
53
|
+
// it is that the model still has the first one.
|
|
54
|
+
{ stubReply: ["Good to meet you, Sam.", "You said Berlin."] },
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
test(
|
|
58
|
+
"keeps a reply speakable, even when the question invites an essay",
|
|
59
|
+
async ({ session }) => {
|
|
60
|
+
const turn = await session.say(
|
|
61
|
+
"Tell me everything you know about the history of the Roman Empire.",
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// This template ships no prompt of its own, so what holds the reply down
|
|
65
|
+
// is `DEFAULT_SYSTEM_PROMPT`'s SPEAKING section — two sentences, about
|
|
66
|
+
// thirty spoken words, no markdown. That is a measured rule (interruption
|
|
67
|
+
// rate climbs from 17% under ten words to 59% past thirty-five), and a
|
|
68
|
+
// stage swap that quietly loses it produces an agent nobody can hold a
|
|
69
|
+
// call with. The ceiling is generous against the rule's own thirty so the
|
|
70
|
+
// case fails on an essay rather than on a long sentence.
|
|
71
|
+
expect(wordCount(turn.text)).toBeLessThanOrEqual(80);
|
|
72
|
+
expect(turn.text).not.toMatch(/[*#`]|^\s*[-•]\s/m);
|
|
73
|
+
},
|
|
74
|
+
// Live only: a scripted reply's length is this file's own choice, so
|
|
75
|
+
// asserting it in stub mode would measure nothing.
|
|
76
|
+
{ live: true },
|
|
77
|
+
);
|
|
78
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agent } from "@alexkroman1/aai";
|
|
2
|
-
import {
|
|
2
|
+
import { anthropicLlm } from "@alexkroman1/aai/llm";
|
|
3
3
|
|
|
4
4
|
export default agent({
|
|
5
5
|
name: "pipeline-simple",
|
|
@@ -8,5 +8,5 @@ export default agent({
|
|
|
8
8
|
// left unset (STT and TTS here) runs on the AssemblyAI default, billed to
|
|
9
9
|
// the one key a published agent is guaranteed to have, so swapping a stage
|
|
10
10
|
// never means restating the other two.
|
|
11
|
-
llm:
|
|
11
|
+
llm: anthropicLlm({ model: "claude-haiku-4-5" }),
|
|
12
12
|
});
|