Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions src/lib/sessionSchema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export const importFromPayload = (json: string): z.output<FormSchema> => {
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)
Expand Down
2 changes: 1 addition & 1 deletion src/lib/sessionSchema/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ const formSchema = z.object({
])
.default({
type: 'kill_session',
minimum: 100000
minimum: 1000000
})
}),
tools: z.record(z.string().nonempty(), CustomToolSchema),
Expand Down
14 changes: 13 additions & 1 deletion src/routes/(app)/workbench/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -644,12 +643,25 @@
</Menubar.Root>
</Header>

<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<form
method="POST"
use:enhance
class="flex h-full flex-col gap-2 overflow-hidden p-2 pt-0"
enctype="multipart/form-data"
autocomplete="off"
onkeydown={(e) => {
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'}
<Resizable.PaneGroup
Expand Down
78 changes: 78 additions & 0 deletions src/routes/(app)/workbench/options/CurrencyInput.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<script lang="ts">
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));
}
</script>

<ButtonGroup.Root class="grow">
<ButtonGroup.Text>
<Label.Root>$</Label.Root>
</ButtonGroup.Text>
<InputGroup.Root>
<InputGroup.Input
value={isEditing ? editingValue : formatUSD(dollars)}
onfocus={onFocus}
onblur={onBlur}
{disabled}
{placeholder}
{maxlength}
type="text"
/>
</InputGroup.Root>
</ButtonGroup.Root>
194 changes: 84 additions & 110 deletions src/routes/(app)/workbench/panes/AgentPane.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -95,11 +96,9 @@
return $formData.agents[agentIdx]?.budgetSettings?.exhaustionBehavior;
}

function isKillBehavior(
behavior: ReturnType<typeof getAgentExhaustionBehavior>
): behavior is { type: 'kill'; force: boolean; minimum: number } {
return behavior?.type === 'kill';
}
const agentBehavior = $derived(
$formData.agents[ctx.selectedAgent!]?.budgetSettings?.exhaustionBehavior
);
</script>

{#if ctx.selectedAgent !== null && curAgent && curCatalog}
Expand Down Expand Up @@ -254,14 +253,15 @@
</header>
<ol class="border-t">
<li>
<Accordion.Root type="multiple" value="budget">
<Accordion.Root type="multiple" value={['budget']}>
<Accordion.Item value="budget">
<Accordion.Trigger variant="compact">Agent budget</Accordion.Trigger>

<Accordion.Content class="flex flex-col gap-2 p-0">
{@const agentIdx = ctx.selectedAgent!}
<Form.ElementField
{form}
name="agents[{ctx.selectedAgent!}].budgetSettings.budget"
name="agents[{agentIdx}].budgetSettings.budget"
class="flex items-center gap-2 "
>
<Form.Control>
Expand All @@ -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
</TooltipLabel>
<ButtonGroup.Root class="grow">
<ButtonGroup.Text>
<Label.Root>$</Label.Root>
</ButtonGroup.Text>
<InputGroup.Root>
<InputGroup.Input
{...props}
value={($formData.agents[ctx.selectedAgent!]?.budgetSettings?.budget ??
0) / 100000000}
oninput={(e: { currentTarget: { valueAsNumber: any } }) => {
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"
/>
</InputGroup.Root>
</ButtonGroup.Root>

<CurrencyInput
value={$formData.agents[agentIdx]?.budgetSettings?.budget ?? 0}
onchange={(micro) => {
const agent = $formData.agents[agentIdx];
if (!agent) return;

agent.budgetSettings ??= {};

agent.budgetSettings.budget = micro;

$formData.agents = $formData.agents;
}}
/>
{/snippet}
</Form.Control>
</Form.ElementField>

<Form.ElementField
{form}
name="agents[{ctx.selectedAgent!}].budgetSettings.exhaustionBehavior.type"
name="agents[{agentIdx}].budgetSettings.exhaustionBehavior.type"
class="flex w-full items-center gap-2 "
>
<Form.Control>
{#snippet children()}
{@const agentIdx = ctx.selectedAgent!}
{@const exhaustionBehavior = getAgentExhaustionBehavior(agentIdx)}
<TooltipLabel
title="Exhaustion Behavior"
Expand Down Expand Up @@ -391,13 +384,9 @@
{/snippet}
</Form.Control>
</Form.ElementField>
{#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}
<Form.ElementField
{form}
name="agents[{agentIdx}].budgetSettings.exhaustionBehavior.force"
Expand All @@ -412,87 +401,72 @@
required: true,
type: 'boolean'
}}
class="max-w-1/4 min-w-1/4 "
class="max-w-1/4 min-w-1/4"
>
Force kill agent
</TooltipLabel>
<ButtonGroup.Root {...props} class="m-0 justify-start">
<Button
class={cn(
killBehavior.force !== true ? 'bg-accent text-accent-foreground' : ''
)}
onclick={() => {
const behavior =
$formData.agents[agentIdx]?.budgetSettings?.exhaustionBehavior;
if (behavior?.type === 'kill') {
behavior.force = true;
$formData.agents = $formData.agents;
}
}}>True</Button
>
<Button
class={cn(
killBehavior.force !== false ? 'bg-accent text-accent-foreground' : ''
)}
onclick={() => {
const behavior =
$formData.agents[agentIdx]?.budgetSettings?.exhaustionBehavior;
if (behavior?.type === 'kill') {
behavior.force = false;
$formData.agents = $formData.agents;
}
}}>False</Button
>
</ButtonGroup.Root>
{/snippet}
</Form.Control>
</Form.ElementField>
<Form.ElementField
{form}
name="agents[{agentIdx}].budgetSettings.exhaustionBehavior.minimum"
class="flex items-center gap-2 "
>
<Form.Control>
{#snippet children({ props })}
<TooltipLabel
title="Minimum threshold"
tooltip="If the session budget drops below this, it will trigger the exhaustion behavior. This is used to prevent overclaiming."
extra={{
required: true,
type: 'integer'
<ToggleGroup.Root
{...props}
type="single"
class="w-full grow"
variant="outline"
value={String(force)}
onValueChange={(v: string) => {
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
</TooltipLabel>
<ButtonGroup.Root class="grow">
<ButtonGroup.Text>
<Label.Root>$</Label.Root>
</ButtonGroup.Text>
<InputGroup.Root>
<InputGroup.Input
{...props}
value={(killBehavior.minimum ?? 0) / 100000000}
oninput={(e: { currentTarget: { valueAsNumber: any } }) => {
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"
/>
</InputGroup.Root>
</ButtonGroup.Root>
<ToggleGroup.Item value="true">True</ToggleGroup.Item>

<ToggleGroup.Item value="false">False</ToggleGroup.Item>
</ToggleGroup.Root>
{/snippet}
</Form.Control>
</Form.ElementField>
{#if killBehavior?.type === 'kill'}
<Form.ElementField
{form}
name="agents[{agentIdx}].budgetSettings.exhaustionBehavior.minimum"
class="flex items-center gap-2 "
>
<Form.Control>
{#snippet children({ props })}
<TooltipLabel
title="Minimum threshold"
tooltip="If the session budget drops below this, it will trigger the exhaustion behavior. This is used to prevent overclaiming."
extra={{
required: true,
type: 'integer'
}}
class="max-w-1/4 min-w-1/4"
>
Minimum
</TooltipLabel>
<CurrencyInput
value={killBehavior.minimum ?? 0}
onchange={(micro) => {
killBehavior.minimum = micro;
$formData.agents = $formData.agents;
}}
/>
{/snippet}
</Form.Control>
</Form.ElementField>
{/if}
{/if}
</Accordion.Content>
</Accordion.Item>
Expand Down
Loading
Loading