diff --git a/src/lib/sessionSchema/index.ts b/src/lib/sessionSchema/index.ts index 8df01a48..ceb5657a 100644 --- a/src/lib/sessionSchema/index.ts +++ b/src/lib/sessionSchema/index.ts @@ -183,6 +183,7 @@ export const importFromPayload = (json: string): z.output => { blocking: agent.blocking ?? true, systemPrompt: agent.systemPrompt, plugins: agent.plugins ?? [], + budgetSettings: agent.budgetSettings ?? {}, options: agent.options ? (structuredClone(agent.options) as any) : {}, customToolAccess: new Set( (agent.customToolAccess ?? []).map((t) => toolMap[t]).filter(Boolean) as string[] // typescript is stupid this is safe because .filter(Boolean) diff --git a/src/lib/sessionSchema/types.ts b/src/lib/sessionSchema/types.ts index 077eb906..7c88baca 100644 --- a/src/lib/sessionSchema/types.ts +++ b/src/lib/sessionSchema/types.ts @@ -243,7 +243,7 @@ const formSchema = z.object({ ]) .default({ type: 'kill_session', - minimum: 100000 + minimum: 1000000 }) }), tools: z.record(z.string().nonempty(), CustomToolSchema), diff --git a/src/routes/(app)/workbench/+page.svelte b/src/routes/(app)/workbench/+page.svelte index ee3dbf8a..bca18440 100644 --- a/src/routes/(app)/workbench/+page.svelte +++ b/src/routes/(app)/workbench/+page.svelte @@ -514,7 +514,6 @@ onMount(async () => { if (sessionDraft.current && sessionDraft.current.agentGraphRequest.agents.length >= 0) { - // ensure required 'from' property is present for importSession sessCtx.importSession({ from: JSON.stringify(sessionDraft.current), success: 'Loaded previous workbench draft' @@ -644,12 +643,25 @@ +
{ + if (e.isComposing) return; + if (e.key !== 'Enter') return; + + const el = document.activeElement; + + if (el instanceof HTMLInputElement) { + e.preventDefault(); + el.blur(); + } + }} + onsubmit={(e) => e.preventDefault()} > {#if view === 'workbench'} + import * as ButtonGroup from '@coral-os/component-library/ui/button-group/index.js'; + import * as InputGroup from '@coral-os/component-library/ui/input-group/index.js'; + import * as Label from '@coral-os/component-library/ui/label/index.js'; + import { tick } from 'svelte'; + + const MICRODOLLARS_PER_DOLLAR = 100_000_000; + + const toDollars = (micro: number) => micro / MICRODOLLARS_PER_DOLLAR; + const toMicro = (dollars: number) => Math.round(dollars * MICRODOLLARS_PER_DOLLAR); + + const formatUSD = (value: number) => { + return new Intl.NumberFormat(undefined, { + style: 'currency', + currency: 'USD', + currencyDisplay: 'narrowSymbol', + minimumFractionDigits: 2, + maximumFractionDigits: 8 + }).format(value); + }; + + const parseNumber = (value: string) => Number(value.replace(/[^0-9.-]/g, '')); + + interface Props { + value: number; + disabled?: boolean; + placeholder?: string; + maxlength?: number; + onchange?: (microdollars: number) => void; + } + + let { + value, + disabled = false, + placeholder = '$0.00', + maxlength = 16, + onchange + }: Props = $props(); + + let isEditing = $state(false); + let editingValue = $state(''); + + const dollars = $derived(toDollars(value ?? 0)); + + async function onFocus(e: FocusEvent & { currentTarget: HTMLInputElement }) { + isEditing = true; + editingValue = isNaN(dollars) ? '' : dollars.toFixed(10).replace(/\.?0+$/, ''); + + await tick(); + e.currentTarget.select(); + + // TODO: without the await tick it doesnt select! :) + } + + function onBlur(e: FocusEvent & { currentTarget: HTMLInputElement }) { + isEditing = false; + const parsed = parseNumber(e.currentTarget.value); + const normalized = isNaN(parsed) ? 0 : parsed; + onchange?.(toMicro(normalized)); + } + + + + + $ + + + + + diff --git a/src/routes/(app)/workbench/panes/AgentPane.svelte b/src/routes/(app)/workbench/panes/AgentPane.svelte index 442e42ea..8c43eabd 100644 --- a/src/routes/(app)/workbench/panes/AgentPane.svelte +++ b/src/routes/(app)/workbench/panes/AgentPane.svelte @@ -37,6 +37,7 @@ import { getSessionDataFromTemplateName } from '../templates/TemplateLib'; import { getSessionContext } from '$lib/sessionCreatorContext'; import { cn } from '$lib/utils'; + import CurrencyInput from '../options/CurrencyInput.svelte'; let appCtx = appContext.get(); @@ -95,11 +96,9 @@ return $formData.agents[agentIdx]?.budgetSettings?.exhaustionBehavior; } - function isKillBehavior( - behavior: ReturnType - ): behavior is { type: 'kill'; force: boolean; minimum: number } { - return behavior?.type === 'kill'; - } + const agentBehavior = $derived( + $formData.agents[ctx.selectedAgent!]?.budgetSettings?.exhaustionBehavior + ); {#if ctx.selectedAgent !== null && curAgent && curCatalog} @@ -254,14 +253,15 @@
  1. - + Agent budget + {@const agentIdx = ctx.selectedAgent!} @@ -273,42 +273,35 @@ required: true, type: 'integer' }} - class="max-w-1/4 min-w-1/4 " + class="max-w-1/4 min-w-1/4" > Agent budget - - - $ - - - { - const dollars = e.currentTarget.valueAsNumber; - $formData.agents[ctx.selectedAgent!]!.budgetSettings!.budget = - Number.isNaN(dollars) ? 0 : Math.round(dollars * 100000000); - }} - type="number" - step="0.01" - placeholder="0.00" - /> - - + + { + const agent = $formData.agents[agentIdx]; + if (!agent) return; + + agent.budgetSettings ??= {}; + + agent.budgetSettings.budget = micro; + + $formData.agents = $formData.agents; + }} + /> {/snippet} {#snippet children()} - {@const agentIdx = ctx.selectedAgent!} {@const exhaustionBehavior = getAgentExhaustionBehavior(agentIdx)} - {#if isKillBehavior(getAgentExhaustionBehavior(ctx.selectedAgent!))} - {@const agentIdx = ctx.selectedAgent!} - {@const killBehavior = getAgentExhaustionBehavior(agentIdx) as { - type: 'kill'; - force: boolean; - minimum: number; - }} + {#if agentBehavior?.type !== 'consume_session' && agentBehavior} + {@const killBehavior = getAgentExhaustionBehavior(agentIdx)} + {@const force = killBehavior?.type === 'kill' ? killBehavior.force : false} Force kill agent - - - - - {/snippet} - - - - - {#snippet children({ props })} - { + if (!v) return; + + const agent = $formData.agents[agentIdx]; + if (!agent) return; + + agent.budgetSettings ??= {}; + + const existing = agent.budgetSettings.exhaustionBehavior; + + agent.budgetSettings.exhaustionBehavior = { + type: 'kill', + force: v === 'true', + minimum: existing?.type === 'kill' ? existing.minimum : 0 + }; + + $formData.agents = $formData.agents; }} - class="max-w-1/4 min-w-1/4 " > - Minimum - - - - $ - - - { - const dollars = e.currentTarget.valueAsNumber; - const behavior = - $formData.agents[agentIdx]?.budgetSettings?.exhaustionBehavior; - if (behavior?.type === 'kill') { - behavior.minimum = Number.isNaN(dollars) - ? 0 - : Math.round(dollars * 100000000); - $formData.agents = $formData.agents; - } - }} - type="number" - step="0.01" - placeholder="0.00" - /> - - + True + + False + {/snippet} + {#if killBehavior?.type === 'kill'} + + + {#snippet children({ props })} + + Minimum + + { + killBehavior.minimum = micro; + $formData.agents = $formData.agents; + }} + /> + {/snippet} + + + {/if} {/if} diff --git a/src/routes/(app)/workbench/panes/BudgetPane.svelte b/src/routes/(app)/workbench/panes/BudgetPane.svelte index d3efd873..c518dc45 100644 --- a/src/routes/(app)/workbench/panes/BudgetPane.svelte +++ b/src/routes/(app)/workbench/panes/BudgetPane.svelte @@ -22,6 +22,8 @@ import * as Label from '@coral-os/component-library/ui/label/index.js'; + import CurrencyInput from '../options/CurrencyInput.svelte'; + import { Context } from 'runed'; import { TooltipLabel, TwostepButton } from '@coral-os/component-library'; @@ -99,6 +101,13 @@ 'Once the session budget is exhausted and claimed from, a warning will be produced. This behavior has a high risk of overclaiming.' } ] as const; + + const MICRODOLLARS_PER_DOLLAR = 100_000_000; + + const toDollars = (micro: number) => micro / MICRODOLLARS_PER_DOLLAR; + const toMicro = (dollars: number) => Math.round(dollars * MICRODOLLARS_PER_DOLLAR); + + const isWarnOnly = $derived($formData.sessionBudgetSettings.exhaustionBehavior.type === 'warn'); {#if ctx && $formData} @@ -123,27 +132,12 @@ > Session budget - - - $ - - - { - const dollars = e.currentTarget.valueAsNumber; - - $formData.sessionBudgetSettings.budget = Number.isNaN(dollars) - ? 0 - : Math.round(dollars * 100000000); - }} - type="number" - step="0.01" - placeholder="0.00" - /> - - + { + $formData.sessionBudgetSettings.budget = micro; + }} + /> {/snippet} @@ -230,33 +224,23 @@ > Force kill agent - - - - + { + if (v && $formData.sessionBudgetSettings.exhaustionBehavior.type === 'kill_agent') { + $formData.sessionBudgetSettings.exhaustionBehavior.force = v === 'true'; + } + }} + > + True + + False + {/snippet} @@ -281,32 +265,15 @@ > Minimum - - - $ - - - { - if ($formData.sessionBudgetSettings.exhaustionBehavior.type !== 'warn') { - const dollars = e.currentTarget.valueAsNumber; - - $formData.sessionBudgetSettings.exhaustionBehavior.minimum = Number.isNaN( - dollars - ) - ? 0 - : Math.round(dollars * 100000000); - } - }} - type="number" - step="0.01" - placeholder="0.00" - /> - - + { + if ($formData.sessionBudgetSettings.exhaustionBehavior.type !== 'warn') { + $formData.sessionBudgetSettings.exhaustionBehavior.minimum = micro; + } + }} + /> {/if} {/snippet} @@ -359,98 +326,5 @@

    - - {/if}