diff --git a/apps/agent-forge/e2e/narrow.spec.ts b/apps/agent-forge/e2e/narrow.spec.ts index 912f67690..c1d75aaaa 100644 --- a/apps/agent-forge/e2e/narrow.spec.ts +++ b/apps/agent-forge/e2e/narrow.spec.ts @@ -80,3 +80,16 @@ test('at 1280px the three-column layout has no drawer toggles', async ({ page }) await expect(page.getByRole('button', { name: 'More actions' })).toHaveCount(0); await expect(page.getByRole('button', { name: 'Save' })).toBeVisible(); }); + +test('#/playground at 375px stacks into one column with nothing scrolling sideways', async ({ page }) => { + await page.setViewportSize({ width: 375, height: 812 }); + await page.goto('/?token=e2e-studio-token#/playground'); + const main = page.getByRole('main'); + await expect(main.getByRole('heading', { name: 'Playground' })).toBeVisible(); + expect(await horizontalOverflow(page)).toBe(0); + expect(await main.evaluate((el) => el.scrollWidth - el.clientWidth)).toBe(0); + for (const name of ['Run', 'Add message', 'Save as new prompt version']) { + const box = await main.getByRole('button', { name, exact: true }).boundingBox(); + expect(box && box.x >= 0 && box.x + box.width <= 375, `${name} is on-screen`).toBe(true); + } +}); diff --git a/apps/agent-forge/server/playground.ts b/apps/agent-forge/server/playground.ts index 1e7999483..bcaccac05 100644 --- a/apps/agent-forge/server/playground.ts +++ b/apps/agent-forge/server/playground.ts @@ -23,7 +23,6 @@ import { InMemoryApprovalStore, SDKError, type AgentEvent, - type AgentEventUsage, type AgentSpec, type Message, } from '@lousho/build-ai-agent'; @@ -38,6 +37,7 @@ import { import { buildAgentFromSpec } from './buildAgent'; import { observeTarget } from './observeProxy'; import type { SecretsStore } from './secretsStore'; +import type { PlaygroundEvent, PlaygroundMessage, PlaygroundRunRequest } from '../shared/wireTypes'; /** The agent name every playground run reports under. */ const PLAYGROUND_AGENT_NAME = 'playground'; @@ -47,28 +47,7 @@ const PROMPT_REF_ATTR = 'lousho.prompt.ref'; const ROLES = new Set(['system', 'user', 'assistant']); const MAX_MESSAGES = 200; -interface PlaygroundMessage { - role: 'system' | 'user' | 'assistant'; - content: string; -} - -/** The validated `POST /playground/run` body. */ -interface PlaygroundRunRequest { - messages: PlaygroundMessage[]; - variables?: Record; - /** `provider/model` (`openai/gpt-4o-mini`, `mock`, `ollama/llama3.2`). */ - model: string; - /** Tool names the Studio already resolves (`current-date`, `day-name`, `http`, `web-fetch`, `demo-approval`). */ - tools?: string[]; - promptRef?: string; -} - -/** One SSE `data:` payload of the stream. */ -export type PlaygroundEvent = - | { type: 'delta'; text: string } - | { type: 'tool_call'; id: string; name: string; args: Record } - | { type: 'done'; finishReason: string; text: string; traceId?: string; usage?: AgentEventUsage } - | { type: 'error'; message: string; code?: string }; +export type { PlaygroundEvent }; const isRecord = (value: unknown): value is Record => typeof value === 'object' && value !== null && !Array.isArray(value); diff --git a/apps/agent-forge/shared/wireTypes.ts b/apps/agent-forge/shared/wireTypes.ts index a86fd3cdf..a4a0d58a7 100644 --- a/apps/agent-forge/shared/wireTypes.ts +++ b/apps/agent-forge/shared/wireTypes.ts @@ -10,7 +10,7 @@ * defines the wire shape of the status/events the server reports back to * the app over `GET /agents/:id/status` and `WS /agents/:id/stream`. */ -import type { CheckpointStatus, TrajectoryComparison, TrajectoryStep } from '@lousho/build-ai-agent'; +import type { AgentEventUsage, CheckpointStatus, TrajectoryComparison, TrajectoryStep } from '@lousho/build-ai-agent'; /** * Status pill states the app's agent list / topbar render (as in the @@ -292,3 +292,27 @@ export interface ForkRunResponse { /** `GET /runs/compare?a=&b=` - `compareTrajectories(a, b)`. */ export type RunComparisonPayload = TrajectoryComparison; + +/** One `POST /playground/run` message (server/playground.ts; the Playground screen's editor rows). */ +export interface PlaygroundMessage { + role: 'system' | 'user' | 'assistant'; + content: string; +} + +/** The `POST /playground/run` body. */ +export interface PlaygroundRunRequest { + messages: PlaygroundMessage[]; + variables?: Record; + /** `provider/model` (`openai/gpt-4o-mini`, `mock`, `ollama/llama3.2`). */ + model: string; + /** Tool names the Studio already resolves (`current-date`, `day-name`, `http`, `web-fetch`, `demo-approval`). */ + tools?: string[]; + promptRef?: string; +} + +/** One SSE `data:` payload of the `POST /playground/run` stream; every stream ends with one `done` or `error`. */ +export type PlaygroundEvent = + | { type: 'delta'; text: string } + | { type: 'tool_call'; id: string; name: string; args: Record } + | { type: 'done'; finishReason: string; text: string; traceId?: string; usage?: AgentEventUsage } + | { type: 'error'; message: string; code?: string }; diff --git a/apps/agent-forge/src/observe/observeClient.ts b/apps/agent-forge/src/observe/observeClient.ts index 3b8c6bc87..3172f10bc 100644 --- a/apps/agent-forge/src/observe/observeClient.ts +++ b/apps/agent-forge/src/observe/observeClient.ts @@ -222,6 +222,53 @@ export class ObserveClient { postScore(score: NewScore): Promise<{ score_id: string }> { return this.request('/api/v1/scores', { method: 'POST', body: score }); } + + /** `GET /api/v1/prompts/:name/:ref` (ref = `latest`, a version number or a label); `undefined` on the daemon's 404. */ + async getPrompt(name: string, ref = 'latest'): Promise { + try { + const body = await this.request<{ prompt: ObservePrompt }>( + `/api/v1/prompts/${encodeURIComponent(name)}/${encodeURIComponent(ref)}` + ); + return body.prompt; + } catch (error) { + if (error instanceof ObserveApiError && error.status === 404) return undefined; + throw error; + } + } + + /** `POST /api/v1/prompts` - pushes a new version (`unchanged` when the text matches the latest one). */ + createPrompt(input: NewPromptVersion): Promise { + return this.request('/api/v1/prompts', { method: 'POST', body: input }); + } +} + +/** One registry prompt version, as `GET /api/v1/prompts/:name/:ref` returns it. */ +export interface ObservePrompt { + prompt_id: string; + name: string; + version: number; + labels: string[]; + /** `null` when the version was pushed without a variable list. */ + variables: string[] | null; + hash: string; + text: string; + created_at: number; +} + +/** The body of {@link ObserveClient.createPrompt}. */ +export interface NewPromptVersion { + name: string; + text: string; + labels?: string[]; + variables?: string[]; +} + +/** The answer of {@link ObserveClient.createPrompt}: the stored version, and whether it was new. */ +export interface PromptPushResult { + prompt: ObservePrompt; + created: boolean; + version: number; + unchanged: boolean; } export const observeClient = new ObserveClient(); diff --git a/apps/agent-forge/src/screens/playground/MessageEditor.tsx b/apps/agent-forge/src/screens/playground/MessageEditor.tsx new file mode 100644 index 000000000..0d6b6847a --- /dev/null +++ b/apps/agent-forge/src/screens/playground/MessageEditor.tsx @@ -0,0 +1,72 @@ +import type { PlaygroundMessage, PlaygroundRole } from './types'; + +const ROLES: PlaygroundRole[] = ['system', 'user', 'assistant']; +const MAX_ROWS = 200; + +interface MessageEditorProps { + messages: PlaygroundMessage[]; + onChange: (messages: PlaygroundMessage[]) => void; +} + +/** The message rows: a role select and a text area each, plus remove and "Add message". */ +export function MessageEditor({ messages, onChange }: MessageEditorProps) { + const update = (index: number, patch: Partial) => + onChange(messages.map((m, i) => (i === index ? { ...m, ...patch } : m))); + const remove = (index: number) => onChange(messages.filter((_, i) => i !== index)); + const add = () => { + const last = messages.at(-1)?.role; + onChange([...messages, { role: last === 'user' ? 'assistant' : 'user', content: '' }]); + }; + + return ( +
+

+ Messages +

+
    + {messages.map((message, index) => ( +
  1. +
    + + +
    +