@nuxtagentic/agents 0.0.0-stage → 0.1.1

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.
Files changed (42) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +99 -2
  3. package/module.ts +35 -0
  4. package/package.json +56 -3
  5. package/runtime/actions.ts +22 -0
  6. package/runtime/components/AgentEditor.vue +446 -0
  7. package/runtime/components/AgentList.vue +110 -0
  8. package/runtime/components/AgentToolPicker.vue +371 -0
  9. package/runtime/components/AgentsWorkspace.vue +328 -0
  10. package/runtime/components/ModelSelect.vue +113 -0
  11. package/runtime/components/RunPanel.vue +114 -0
  12. package/runtime/components/RuntimeModelPicker.vue +276 -0
  13. package/runtime/components/SourceEditor.vue +78 -0
  14. package/runtime/composables/useAgentIntegrationTools.ts +24 -0
  15. package/runtime/composables/useAgentRuntimes.ts +32 -0
  16. package/runtime/composables/useAgents.ts +24 -0
  17. package/runtime/index.ts +71 -0
  18. package/runtime/middleware/agents-route.ts +10 -0
  19. package/runtime/pages/agents.vue +26 -0
  20. package/runtime/server/api/agents/[id]/runs/[run].get.ts +11 -0
  21. package/runtime/server/api/agents/[id]/runs.post.ts +33 -0
  22. package/runtime/server/api/agents/runtimes.get.ts +37 -0
  23. package/runtime/server/utils/agent-execution.ts +124 -0
  24. package/runtime/server/utils/agent-gateway.ts +13 -0
  25. package/runtime/server/utils/agent-model-catalog.ts +93 -0
  26. package/runtime/server/utils/agent-provider-keys.ts +46 -0
  27. package/runtime/server/utils/local-agent-catalog.ts +151 -0
  28. package/runtime/server/utils/local-agent-runtime.ts +8 -0
  29. package/runtime/server/utils/model-fetch.ts +66 -0
  30. package/runtime/server/utils/run-local-agent.ts +208 -0
  31. package/runtime/server/utils/tool-result.ts +22 -0
  32. package/runtime/shared/agent-summary.ts +9 -0
  33. package/runtime/shared/agent.ts +4 -0
  34. package/runtime/shared/definition.ts +16 -0
  35. package/runtime/shared/editorTheme.ts +43 -0
  36. package/runtime/shared/integration-tool.ts +11 -0
  37. package/runtime/shared/presentation.ts +50 -0
  38. package/runtime/shared/prompt.ts +37 -0
  39. package/runtime/shared/runtime.ts +40 -0
  40. package/runtime/shared/source.ts +81 -0
  41. package/schema.ts +2 -0
  42. package/server.ts +2 -0
@@ -0,0 +1,110 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import type { AgentSummary } from '../shared/agent-summary'
4
+ import { getAgentAvatarClass, getAgentInitials } from '../shared/presentation'
5
+ import { PROVIDER_LABELS } from '../shared/runtime'
6
+
7
+ const { agents, selectedId, hasSelection, pending, error } = defineProps<{
8
+ agents: AgentSummary[]
9
+ selectedId: number | null
10
+ hasSelection?: boolean
11
+ pending?: boolean
12
+ error?: string
13
+ }>()
14
+
15
+ const emit = defineEmits<{
16
+ select: [id: number]
17
+ refresh: []
18
+ }>()
19
+
20
+ const search = defineModel<string>('search', { required: true })
21
+
22
+ const isHiddenOnMobile = computed(() => hasSelection ?? selectedId !== null)
23
+ const isLoading = computed(() => pending && !agents.length)
24
+ const isEmpty = computed(() => !pending && !error && !agents.length)
25
+ const emptyMessage = computed(() =>
26
+ search.value ? 'No agents match.' : 'Your agents will appear here.',
27
+ )
28
+ </script>
29
+
30
+ <template>
31
+ <aside
32
+ class="flex w-full shrink-0 flex-col border-r border-default md:w-60"
33
+ :class="isHiddenOnMobile ? 'hidden md:flex' : 'flex'"
34
+ >
35
+ <div class="px-3 pt-3 pb-2">
36
+ <UInput
37
+ v-model="search"
38
+ icon="i-lucide-search"
39
+ placeholder="Search agents"
40
+ aria-label="Search agents"
41
+ class="w-full"
42
+ :ui="{
43
+ base: 'rounded-lg text-[13px] ring-(--gray-500)',
44
+ leadingIcon: 'size-3.5 text-dimmed',
45
+ }"
46
+ />
47
+ </div>
48
+ <div class="min-h-0 flex-1 space-y-0.5 overflow-auto px-2 pb-30">
49
+ <div
50
+ v-if="error"
51
+ class="space-y-2 px-2 py-6"
52
+ >
53
+ <p
54
+ role="alert"
55
+ class="text-[13px] text-error"
56
+ >
57
+ {{ error }}
58
+ </p>
59
+ <UButton
60
+ label="Retry"
61
+ color="neutral"
62
+ variant="outline"
63
+ size="xs"
64
+ @click="emit('refresh')"
65
+ />
66
+ </div>
67
+ <p
68
+ v-else-if="isLoading"
69
+ role="status"
70
+ class="px-2 py-6 text-center text-[13px] text-muted"
71
+ >
72
+ Loading agents…
73
+ </p>
74
+ <button
75
+ v-for="agent in agents"
76
+ :key="agent.id"
77
+ type="button"
78
+ :aria-current="selectedId === agent.id ? 'true' : undefined"
79
+ class="flex h-13 w-full cursor-pointer items-center gap-2.5 rounded-lg pr-2.5 pl-2 text-left transition-[background-color,transform] duration-160 ease-[cubic-bezier(.2,0,0,1)] hover:bg-(--gray-400) focus-visible:outline-2 focus-visible:outline-accented active:scale-[0.98] motion-reduce:transition-none"
80
+ :class="selectedId === agent.id && 'bg-(--gray-400)'"
81
+ @click="emit('select', agent.id)"
82
+ >
83
+ <span
84
+ class="grid size-8 shrink-0 place-items-center rounded-full text-xs font-semibold text-black"
85
+ :class="getAgentAvatarClass(agent.id)"
86
+ >
87
+ {{ getAgentInitials(agent.name) }}
88
+ </span>
89
+ <span class="flex min-w-0 flex-1 flex-col gap-0.5">
90
+ <span class="truncate text-sm leading-4.5 text-highlighted">{{ agent.name }}</span>
91
+ <span class="truncate text-xs leading-4 text-dimmed">
92
+ {{ PROVIDER_LABELS[agent.provider] }} · {{ agent.model }}
93
+ </span>
94
+ </span>
95
+ <span
96
+ class="size-1.5 shrink-0 rounded-full transition-colors duration-240"
97
+ :class="agent.status === 'active' ? 'bg-success' : 'bg-(--gray-600)'"
98
+ :aria-label="agent.status === 'active' ? 'Active' : 'Draft'"
99
+ />
100
+ </button>
101
+ <p
102
+ v-if="isEmpty"
103
+ class="px-2 py-6 text-center text-[13px] text-muted"
104
+ >
105
+ {{ emptyMessage }}
106
+ </p>
107
+ </div>
108
+ <slot name="footer" />
109
+ </aside>
110
+ </template>
@@ -0,0 +1,371 @@
1
+ <script setup lang="ts">
2
+ import { computed, reactive, ref } from 'vue'
3
+ import {
4
+ AGENT_PARAMETER_TYPES,
5
+ agentToolSchema,
6
+ type AgentDefinition,
7
+ type AgentTool,
8
+ } from '@cloudconfig/core/agents'
9
+ import { PROVIDER_LABELS } from '../shared/runtime'
10
+ import type { AgentIntegrationTool } from '../shared/integration-tool'
11
+ import { useAgentIntegrationTools } from '../composables/useAgentIntegrationTools'
12
+
13
+ const { integrationTools } = defineProps<{ integrationTools?: AgentIntegrationTool[] }>()
14
+ const agent = defineModel<AgentDefinition>({ required: true })
15
+ defineEmits<{ close: [] }>()
16
+ const CHOICES = [
17
+ { value: 'auto', label: 'Auto', hint: 'The model decides when to call a tool.' },
18
+ {
19
+ value: 'required',
20
+ label: 'Required',
21
+ hint: 'The model must call at least one tool each turn.',
22
+ },
23
+ { value: 'none', label: 'None', hint: 'Tools are described but never called.' },
24
+ ] as const
25
+ const expanded = ref<string | null>(null)
26
+ // Forms render once a tool is first opened, so long tool lists stay cheap to mount.
27
+ const opened = reactive(new Set<string>())
28
+ const copied = ref<string | null>(null)
29
+ const copyError = ref('')
30
+ let copiedTimer: ReturnType<typeof setTimeout> | undefined
31
+ const integrations = integrationTools ? undefined : useAgentIntegrationTools()
32
+ const integrationError = computed(() => integrations?.error.value)
33
+ const enabled = computed(() => agent.value.tools.filter((tool) => tool.enabled).length)
34
+ const choice = computed(() =>
35
+ Math.max(
36
+ 0,
37
+ CHOICES.findIndex((item) => item.value === agent.value.toolChoice),
38
+ ),
39
+ )
40
+ const suggestions = computed(() =>
41
+ (integrationTools ?? integrations?.data.value ?? []).filter(
42
+ (item) => !agent.value.tools.some((tool) => tool.name === item.name),
43
+ ),
44
+ )
45
+ const fieldUi = {
46
+ base: 'h-8 rounded-lg px-2.5 text-[13px] ring-(--gray-500) focus-visible:ring-(--gray-800)',
47
+ }
48
+
49
+ function toggle(tool: AgentTool) {
50
+ opened.add(tool.id)
51
+ expanded.value = expanded.value === tool.id ? null : tool.id
52
+ }
53
+ function addTool(tool: AgentTool) {
54
+ agent.value.tools.push(tool)
55
+ opened.add(tool.id)
56
+ expanded.value = tool.id
57
+ }
58
+ function addBlankTool() {
59
+ let name = 'new_tool'
60
+ for (let index = 2; agent.value.tools.some((tool) => tool.name === name); index++) {
61
+ name = `new_tool_${index}`
62
+ }
63
+ addTool({ id: crypto.randomUUID(), name, description: '', enabled: true, parameters: [] })
64
+ }
65
+ function addIntegrationTool(item: AgentIntegrationTool) {
66
+ addTool({
67
+ id: crypto.randomUUID(),
68
+ name: item.name,
69
+ description: item.description,
70
+ enabled: true,
71
+ parameters: Object.entries(item.parameters.properties).map(([name, field]) => ({
72
+ name,
73
+ type: field.type,
74
+ description: field.description ?? '',
75
+ required: item.parameters.required.includes(name),
76
+ })),
77
+ })
78
+ }
79
+ function addParameter(tool: AgentTool) {
80
+ let name = 'parameter'
81
+ for (let index = 2; tool.parameters.some((parameter) => parameter.name === name); index++) {
82
+ name = `parameter_${index}`
83
+ }
84
+ tool.parameters.push({ name, type: 'string', description: '', required: false })
85
+ }
86
+ function identifier(value: string) {
87
+ return value.replace(/\s+/g, '_').replace(/[^\w-]/g, '')
88
+ }
89
+ function schema(tool: AgentTool) {
90
+ return JSON.stringify(agentToolSchema(tool, agent.value.provider), null, 2)
91
+ }
92
+ async function copy(tool: AgentTool) {
93
+ copyError.value = ''
94
+ try {
95
+ await navigator.clipboard.writeText(schema(tool))
96
+ copied.value = tool.id
97
+ clearTimeout(copiedTimer)
98
+ copiedTimer = setTimeout(() => (copied.value = null), 1400)
99
+ } catch {
100
+ copyError.value = 'Could not copy. Select the schema text and copy it.'
101
+ }
102
+ }
103
+ </script>
104
+
105
+ <template>
106
+ <div class="flex h-full min-h-0 w-full flex-col">
107
+ <div class="flex h-13 shrink-0 items-center gap-2.5 pr-2 pl-4">
108
+ <span class="text-[15px] text-highlighted">Tools</span>
109
+ <span class="flex-1 text-xs text-dimmed">
110
+ {{ enabled }} of {{ agent.tools.length }} enabled
111
+ </span>
112
+ <UButton
113
+ icon="i-lucide-x"
114
+ aria-label="Close tools"
115
+ variant="ghost"
116
+ square
117
+ class="size-7.5 hover:bg-(--gray-400)"
118
+ @click="$emit('close')"
119
+ />
120
+ </div>
121
+ <div class="flex shrink-0 flex-col gap-1.5 border-b border-default px-4 pb-3">
122
+ <div
123
+ class="relative grid h-7.5 grid-cols-3 rounded-lg border border-(--gray-500) p-0.5"
124
+ role="group"
125
+ aria-label="Tool choice"
126
+ >
127
+ <span
128
+ class="pointer-events-none absolute top-0.5 bottom-0.5 left-0.5 w-[calc((100%-4px)/3)] rounded-md bg-(--gray-400) transition-transform duration-240 ease-[cubic-bezier(.2,0,0,1)] motion-reduce:transition-none"
129
+ :class="['translate-x-0', 'translate-x-full', 'translate-x-[200%]'][choice]"
130
+ />
131
+ <button
132
+ v-for="item in CHOICES"
133
+ :key="item.value"
134
+ type="button"
135
+ class="relative cursor-pointer rounded-md text-xs transition-colors duration-200 focus-visible:outline-2 focus-visible:outline-accented motion-reduce:transition-none"
136
+ :class="
137
+ agent.toolChoice === item.value
138
+ ? 'text-highlighted'
139
+ : 'text-muted hover:text-highlighted'
140
+ "
141
+ :aria-pressed="agent.toolChoice === item.value"
142
+ @click="agent.toolChoice = item.value"
143
+ >
144
+ {{ item.label }}
145
+ </button>
146
+ </div>
147
+ <p class="text-xs text-dimmed">{{ CHOICES[choice]!.hint }}</p>
148
+ </div>
149
+ <div class="min-h-0 flex-1 overflow-y-auto">
150
+ <div
151
+ v-for="tool in agent.tools"
152
+ :key="tool.id"
153
+ class="border-b border-default transition-colors duration-220 motion-reduce:transition-none"
154
+ :class="expanded === tool.id && 'bg-(--gray-300)'"
155
+ >
156
+ <div class="flex h-14 items-center gap-2.5 pr-2 pl-4">
157
+ <USwitch
158
+ v-model="tool.enabled"
159
+ :aria-label="`Enable ${tool.name}`"
160
+ size="xs"
161
+ />
162
+ <button
163
+ type="button"
164
+ class="flex h-11 min-w-0 flex-1 cursor-pointer items-center gap-2.5 rounded-lg px-1.5 text-left transition-colors hover:bg-(--gray-400) focus-visible:outline-2 focus-visible:outline-accented motion-reduce:transition-none"
165
+ :aria-expanded="expanded === tool.id"
166
+ @click="toggle(tool)"
167
+ >
168
+ <span class="flex min-w-0 flex-1 flex-col gap-0.5">
169
+ <span
170
+ class="truncate font-mono text-[13px] leading-4.5 transition-colors"
171
+ :class="tool.enabled ? 'text-highlighted' : 'text-dimmed'"
172
+ >
173
+ {{ tool.name || 'untitled_tool' }}
174
+ </span>
175
+ <span class="truncate text-xs leading-4 text-dimmed">
176
+ {{ tool.description || 'No description' }}
177
+ </span>
178
+ </span>
179
+ <UIcon
180
+ name="i-lucide-chevron-down"
181
+ class="size-4 shrink-0 text-muted transition-transform duration-240 ease-[cubic-bezier(.2,0,0,1)] motion-reduce:transition-none"
182
+ :class="expanded === tool.id && 'rotate-180'"
183
+ />
184
+ </button>
185
+ </div>
186
+ <div
187
+ class="grid transition-[grid-template-rows] duration-260 ease-[cubic-bezier(.2,0,0,1)] motion-reduce:transition-none"
188
+ :class="expanded === tool.id ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'"
189
+ :inert="expanded !== tool.id"
190
+ >
191
+ <div class="min-h-0 overflow-hidden">
192
+ <div
193
+ v-if="opened.has(tool.id)"
194
+ class="flex flex-col gap-3.5 px-4 pt-0.5 pb-4"
195
+ >
196
+ <UFormField
197
+ label="Name"
198
+ :ui="{ label: 'text-xs', container: 'mt-1.5' }"
199
+ >
200
+ <UInput
201
+ :model-value="tool.name"
202
+ placeholder="tool_name"
203
+ class="w-full"
204
+ :ui="{ base: `${fieldUi.base} font-mono text-xs` }"
205
+ @update:model-value="tool.name = identifier(String($event))"
206
+ />
207
+ </UFormField>
208
+ <UFormField
209
+ label="Description"
210
+ :ui="{ label: 'text-xs', container: 'mt-1.5' }"
211
+ >
212
+ <UInput
213
+ v-model="tool.description"
214
+ placeholder="What the tool does and when to call it"
215
+ class="w-full"
216
+ :ui="fieldUi"
217
+ />
218
+ </UFormField>
219
+ <div class="flex flex-col gap-1.5">
220
+ <span class="text-xs text-muted">Parameters</span>
221
+ <div
222
+ v-for="(parameter, index) in tool.parameters"
223
+ :key="index"
224
+ class="flex flex-col gap-1.5 rounded-[10px] border border-default p-1.5"
225
+ >
226
+ <div class="grid grid-cols-[minmax(0,1fr)_72px_76px_28px] items-center gap-1.5">
227
+ <UInput
228
+ :model-value="parameter.name"
229
+ aria-label="Parameter name"
230
+ placeholder="name"
231
+ :ui="{ base: `${fieldUi.base} font-mono text-xs` }"
232
+ @update:model-value="parameter.name = identifier(String($event))"
233
+ />
234
+ <USelect
235
+ v-model="parameter.type"
236
+ :items="[...AGENT_PARAMETER_TYPES]"
237
+ aria-label="Parameter type"
238
+ :ui="{
239
+ base: `${fieldUi.base} px-2 font-mono text-xs`,
240
+ trailingIcon: 'hidden',
241
+ content: 'min-w-28',
242
+ }"
243
+ />
244
+ <button
245
+ type="button"
246
+ class="h-8 cursor-pointer rounded-lg border border-(--gray-500) text-xs transition-colors duration-180 hover:text-highlighted motion-reduce:transition-none"
247
+ :class="
248
+ parameter.required ? 'bg-(--gray-500) text-highlighted' : 'text-dimmed'
249
+ "
250
+ :aria-pressed="parameter.required"
251
+ @click="parameter.required = !parameter.required"
252
+ >
253
+ {{ parameter.required ? 'Required' : 'Optional' }}
254
+ </button>
255
+ <UButton
256
+ icon="i-lucide-x"
257
+ aria-label="Remove parameter"
258
+ variant="ghost"
259
+ square
260
+ class="hover:bg-(--gray-400)"
261
+ @click="tool.parameters.splice(index, 1)"
262
+ />
263
+ </div>
264
+ <UInput
265
+ v-model="parameter.description"
266
+ aria-label="Parameter description"
267
+ placeholder="Description"
268
+ variant="none"
269
+ :ui="{ base: 'h-7 px-1 text-[13px] text-default' }"
270
+ />
271
+ <p
272
+ v-if="parameter.type === 'array'"
273
+ class="px-1 text-xs text-dimmed"
274
+ >
275
+ A list of strings.
276
+ </p>
277
+ </div>
278
+ <p
279
+ v-if="!tool.parameters.length"
280
+ class="text-xs text-dimmed"
281
+ >
282
+ No parameters. The tool is called with an empty object.
283
+ </p>
284
+ <div>
285
+ <UButton
286
+ label="Add parameter"
287
+ icon="i-lucide-plus"
288
+ variant="ghost"
289
+ size="xs"
290
+ class="-ml-2 h-7 px-2 font-normal hover:bg-(--gray-400)"
291
+ @click="addParameter(tool)"
292
+ />
293
+ </div>
294
+ </div>
295
+ <div class="flex flex-col gap-1.5">
296
+ <div class="flex items-center justify-between">
297
+ <span class="text-xs text-muted">
298
+ Schema · {{ PROVIDER_LABELS[agent.provider] }} format
299
+ </span>
300
+ <UButton
301
+ :label="copied === tool.id ? 'Copied' : 'Copy'"
302
+ variant="ghost"
303
+ size="xs"
304
+ class="font-normal hover:bg-(--gray-400)"
305
+ @click="copy(tool)"
306
+ />
307
+ </div>
308
+ <pre
309
+ class="max-h-60 overflow-auto rounded-lg border border-default bg-(--gray-100) px-3.5 py-3 font-mono text-xs leading-4.5 text-default"
310
+ >{{ schema(tool) }}</pre>
311
+ <p
312
+ v-if="copyError"
313
+ role="alert"
314
+ class="text-xs text-error"
315
+ >
316
+ {{ copyError }}
317
+ </p>
318
+ </div>
319
+ <div class="flex justify-end">
320
+ <UButton
321
+ label="Remove tool"
322
+ variant="outline"
323
+ size="xs"
324
+ class="h-7 rounded-lg px-2.5 font-normal text-muted ring-(--gray-500) hover:bg-(--gray-400)"
325
+ @click="agent.tools = agent.tools.filter((item) => item.id !== tool.id)"
326
+ />
327
+ </div>
328
+ </div>
329
+ </div>
330
+ </div>
331
+ </div>
332
+ <p
333
+ v-if="!agent.tools.length"
334
+ class="px-4 py-6 text-center text-[13px] text-dimmed"
335
+ >
336
+ No tools yet. The agent can only reply with text.
337
+ </p>
338
+ </div>
339
+ <div class="flex shrink-0 flex-wrap items-center gap-1.5 border-t border-default p-2">
340
+ <UButton
341
+ label="Add tool"
342
+ icon="i-lucide-plus"
343
+ variant="ghost"
344
+ class="h-7.5 rounded-lg px-2.5 font-normal text-highlighted hover:bg-(--gray-400)"
345
+ @click="addBlankTool"
346
+ />
347
+ <button
348
+ v-for="item in suggestions"
349
+ :key="item.name"
350
+ type="button"
351
+ :title="item.description"
352
+ class="h-6.5 cursor-pointer rounded-full border border-(--gray-500) px-2.5 font-mono text-xs whitespace-nowrap text-muted transition-colors hover:bg-(--gray-400) hover:text-highlighted focus-visible:outline-2 focus-visible:outline-accented active:scale-[0.97] motion-reduce:transition-none"
353
+ @click="addIntegrationTool(item)"
354
+ >
355
+ + {{ item.name }}
356
+ </button>
357
+ <p
358
+ v-if="integrationError"
359
+ class="flex items-center gap-2 px-2 text-xs text-error"
360
+ >
361
+ Could not load integration tools.
362
+ <UButton
363
+ label="Retry"
364
+ variant="link"
365
+ size="xs"
366
+ @click="integrations?.refresh()"
367
+ />
368
+ </p>
369
+ </div>
370
+ </div>
371
+ </template>