@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,328 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, ref, watch } from 'vue'
3
+ import { CloudConfigClientError } from '@cloudconfig/core/client'
4
+ import { createAgentDefinition, type AgentDefinition } from '@cloudconfig/core/agents'
5
+ import { useWorkspaceRecord } from '@nuxtagentic/data/runtime/composables/useWorkspaceRecord'
6
+ import type { WorkspaceEntryContext } from '@nuxtagentic/workspace/runtime/types'
7
+ import { toAgentDefinition, useAgents, type AgentRecord } from '../composables/useAgents'
8
+ import AgentEditor from './AgentEditor.vue'
9
+ import AgentList from './AgentList.vue'
10
+ import RunPanel from './RunPanel.vue'
11
+
12
+ const { context, active } = defineProps<{ context: WorkspaceEntryContext; active: boolean }>()
13
+ const { navigation } = context
14
+ const selection = computed(() => navigation.read('agent'))
15
+ const creating = computed(() => selection.value === 'new')
16
+ const selectedId = computed(() => (/^\d+$/.test(selection.value) ? Number(selection.value) : null))
17
+ const search = computed(
18
+ () =>
19
+ navigation.read('agentSearch') ||
20
+ (!navigation.module.value ? navigation.prompt.value.replace(/^agents?\s*/i, '') : ''),
21
+ )
22
+ const page = computed(() => Math.max(1, Number(navigation.read('agentPage')) || 1))
23
+ const records = useAgents(computed(() => ({ search: search.value, page: page.value })))
24
+ const detail = useWorkspaceRecord<AgentRecord>(
25
+ 'agents',
26
+ { id: selectedId, scope: records.scope, get: (id) => records.api.get(id) },
27
+ { server: true },
28
+ )
29
+ const selected = computed(() =>
30
+ detail.data.value?.id === selectedId.value ? detail.data.value : null,
31
+ )
32
+ const definition = computed(() => (selected.value ? toAgentDefinition(selected.value) : null))
33
+ const isBusy = ref(false)
34
+ const error = ref('')
35
+ const isDeleting = ref(false)
36
+ const isTesting = ref(false)
37
+
38
+ // `?agent=new` asks for a new agent; it is replaced by the created agent's id.
39
+ onMounted(() => {
40
+ if (creating.value) {
41
+ void insert()
42
+ }
43
+ })
44
+ watch(selection, (value) => {
45
+ error.value = ''
46
+ isDeleting.value = false
47
+ if (value === 'new') {
48
+ void insert()
49
+ }
50
+ })
51
+
52
+ function setSearch(value: string | number) {
53
+ return navigation.navigate(
54
+ { agentSearch: String(value) || undefined, agentPage: undefined },
55
+ true,
56
+ )
57
+ }
58
+ function create() {
59
+ if (creating.value) {
60
+ return insert()
61
+ }
62
+ return navigation.navigate({ module: 'agents', agent: 'new', agentName: undefined })
63
+ }
64
+ function errorMessage(cause: unknown) {
65
+ if (cause instanceof CloudConfigClientError && cause.issues.length) {
66
+ return cause.issues.map((issue) => issue.message).join(' ')
67
+ }
68
+ return cause instanceof Error ? cause.message : 'Could not save this agent. Try again.'
69
+ }
70
+ async function run<T>(action: () => Promise<T>): Promise<T | undefined> {
71
+ if (isBusy.value) {
72
+ return
73
+ }
74
+ isBusy.value = true
75
+ error.value = ''
76
+ try {
77
+ return await action()
78
+ } catch (cause) {
79
+ error.value = errorMessage(cause)
80
+ } finally {
81
+ isBusy.value = false
82
+ }
83
+ }
84
+ function insert() {
85
+ return run(async () => {
86
+ const agent = await records.api.create(
87
+ createAgentDefinition(navigation.read('agentName') || 'Untitled agent'),
88
+ )
89
+ await navigation.navigate({ agent: String(agent.id), agentName: undefined }, true)
90
+ await records.refreshAll()
91
+ })
92
+ }
93
+ async function update(agent: AgentDefinition) {
94
+ const id = selectedId.value
95
+ if (id === null) {
96
+ return false
97
+ }
98
+ const saved = await run(async () => {
99
+ detail.data.value = await records.api.update(id, agent)
100
+ await records.refreshAll()
101
+ return true
102
+ })
103
+ return saved ?? false
104
+ }
105
+ function duplicate(agent: AgentDefinition) {
106
+ return run(async () => {
107
+ const copy = await records.api.create({
108
+ ...agent,
109
+ status: 'draft',
110
+ markdown: agent.markdown.replace(/^#\s+(.+)$/m, '# $1 copy'),
111
+ })
112
+ await navigation.navigate({ module: 'agents', agent: String(copy.id) })
113
+ await records.refreshAll()
114
+ })
115
+ }
116
+ function remove() {
117
+ const id = selectedId.value
118
+ if (id === null) {
119
+ return
120
+ }
121
+ return run(async () => {
122
+ await records.api.delete(id)
123
+ await navigation.closeDetail()
124
+ await records.refreshAll()
125
+ })
126
+ }
127
+ </script>
128
+
129
+ <template>
130
+ <ClientOnly>
131
+ <Teleport
132
+ v-if="active"
133
+ defer
134
+ to="#workspace-navbar-actions"
135
+ >
136
+ <div class="flex items-center gap-2">
137
+ <UButton
138
+ v-if="selectedId"
139
+ label="Test agent"
140
+ icon="i-lucide-play"
141
+ color="neutral"
142
+ variant="outline"
143
+ size="sm"
144
+ class="h-8 w-28 justify-center rounded-lg px-3 text-[13px]"
145
+ :ui="{ leadingIcon: 'size-3.5' }"
146
+ @click="isTesting = true"
147
+ />
148
+ <UButton
149
+ label="New agent"
150
+ icon="i-lucide-plus"
151
+ color="neutral"
152
+ size="sm"
153
+ class="h-8 w-28 justify-center rounded-lg px-3 text-[13px]"
154
+ :ui="{ leadingIcon: 'size-3.5' }"
155
+ @click="create"
156
+ />
157
+ </div>
158
+ </Teleport>
159
+ </ClientOnly>
160
+ <div class="relative flex h-full min-h-0 bg-(--gray-200)">
161
+ <AgentList
162
+ :search="search"
163
+ :agents="records.rows.value"
164
+ :selected-id="selectedId"
165
+ :has-selection="!!selection"
166
+ :pending="records.isLoading.value"
167
+ :error="records.error.value?.message"
168
+ @update:search="setSearch"
169
+ @select="navigation.navigate({ agent: String($event) })"
170
+ @refresh="records.refresh()"
171
+ >
172
+ <template #footer>
173
+ <UPagination
174
+ v-if="records.totalCount.value > 50"
175
+ :page="page"
176
+ :total="records.totalCount.value"
177
+ :items-per-page="50"
178
+ :sibling-count="0"
179
+ :show-edges="false"
180
+ size="xs"
181
+ class="shrink-0 border-t border-default p-2"
182
+ @update:page="navigation.navigate({ agentPage: String($event) })"
183
+ />
184
+ </template>
185
+ </AgentList>
186
+ <div
187
+ class="min-h-0 min-w-0 flex-1"
188
+ :class="!selection && 'hidden md:block'"
189
+ >
190
+ <AgentEditor
191
+ v-if="definition"
192
+ :key="selection"
193
+ :agent="definition"
194
+ :active="active"
195
+ :busy="isBusy"
196
+ :error="error"
197
+ :save="update"
198
+ :removing="isDeleting"
199
+ @duplicate="duplicate"
200
+ @remove="isDeleting = true"
201
+ @close="navigation.closeDetail()"
202
+ />
203
+ <div
204
+ v-else-if="selection"
205
+ class="flex h-full flex-col items-center justify-center gap-3 p-6"
206
+ >
207
+ <p
208
+ v-if="creating"
209
+ :role="error ? 'alert' : undefined"
210
+ class="text-sm text-muted"
211
+ >
212
+ {{ error || 'Creating agent…' }}
213
+ </p>
214
+ <p
215
+ v-else
216
+ :role="detail.error.value ? 'alert' : undefined"
217
+ class="text-sm text-muted"
218
+ >
219
+ {{
220
+ detail.error.value?.message ||
221
+ (detail.status.value === 'success' ? 'Agent not found.' : 'Loading agent…')
222
+ }}
223
+ </p>
224
+ <div class="flex gap-2">
225
+ <UButton
226
+ v-if="creating && error"
227
+ label="Try again"
228
+ color="neutral"
229
+ @click="create"
230
+ />
231
+ <UButton
232
+ label="Back to agents"
233
+ color="neutral"
234
+ variant="outline"
235
+ @click="navigation.closeDetail()"
236
+ />
237
+ </div>
238
+ </div>
239
+ <div
240
+ v-else
241
+ class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center px-6 pb-24 text-center"
242
+ >
243
+ <div class="pointer-events-auto flex max-w-80 flex-col items-center gap-3">
244
+ <p
245
+ v-if="records.isLoading.value && !records.rows.value.length"
246
+ role="status"
247
+ class="text-sm text-muted"
248
+ >
249
+ Loading agents…
250
+ </p>
251
+ <template v-else-if="records.error.value">
252
+ <p
253
+ role="alert"
254
+ class="text-sm text-error"
255
+ >
256
+ Could not load agents.
257
+ </p>
258
+ <UButton
259
+ label="Try again"
260
+ color="neutral"
261
+ variant="outline"
262
+ @click="records.refresh()"
263
+ />
264
+ </template>
265
+ <template v-else>
266
+ <p class="text-base text-highlighted">
267
+ {{ records.totalCount.value ? 'Select an agent' : 'No agents yet' }}
268
+ </p>
269
+ <p class="text-[13px] text-pretty text-muted">
270
+ An agent is a Markdown file: its scope, boundaries, decision-making and tool use.
271
+ </p>
272
+ <UButton
273
+ label="New agent"
274
+ class="h-8 rounded-lg px-3"
275
+ @click="create"
276
+ />
277
+ </template>
278
+ </div>
279
+ </div>
280
+ </div>
281
+ </div>
282
+ <UModal
283
+ v-model:open="isTesting"
284
+ title="Test saved agent"
285
+ >
286
+ <template #body>
287
+ <RunPanel
288
+ v-if="selectedId"
289
+ :key="selectedId"
290
+ :agent-id="selectedId"
291
+ />
292
+ </template>
293
+ </UModal>
294
+ <UModal
295
+ v-model:open="isDeleting"
296
+ title="Delete agent?"
297
+ :dismissible="!isBusy"
298
+ >
299
+ <template #body>
300
+ <p class="text-sm text-muted">
301
+ {{ selected?.name }} and its tool declarations will be permanently deleted.
302
+ </p>
303
+ <UAlert
304
+ v-if="error"
305
+ :title="error"
306
+ color="error"
307
+ class="mt-3"
308
+ />
309
+ </template>
310
+ <template #footer>
311
+ <div class="flex w-full justify-end gap-2">
312
+ <UButton
313
+ label="Cancel"
314
+ color="neutral"
315
+ variant="outline"
316
+ :disabled="isBusy"
317
+ @click="isDeleting = false"
318
+ />
319
+ <UButton
320
+ label="Delete agent"
321
+ color="error"
322
+ :loading="isBusy"
323
+ @click="remove"
324
+ />
325
+ </div>
326
+ </template>
327
+ </UModal>
328
+ </template>
@@ -0,0 +1,113 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from 'vue'
3
+ import type { DropdownMenuItem } from '@nuxt/ui'
4
+ import type { AgentModelOption } from '../shared/runtime'
5
+ import { MENU_UI, PILL_CLASS } from '../shared/presentation'
6
+
7
+ const value = defineModel<string>({ required: true })
8
+ const {
9
+ models: availableModels,
10
+ runtimeDefault,
11
+ loading,
12
+ field,
13
+ } = defineProps<{
14
+ models: AgentModelOption[]
15
+ /** Offer the CLI's own default as the empty value. */
16
+ runtimeDefault?: boolean
17
+ loading?: boolean
18
+ /** Full-width form field instead of a toolbar pill. */
19
+ field?: boolean
20
+ }>()
21
+ const emit = defineEmits<{ open: [] }>()
22
+ const isOpen = ref(false)
23
+ const term = ref('')
24
+
25
+ watch(isOpen, (isOpen) => {
26
+ if (isOpen) {
27
+ emit('open')
28
+ } else {
29
+ term.value = ''
30
+ }
31
+ })
32
+ const options = computed(() => {
33
+ const models = [...availableModels]
34
+ if (runtimeDefault && !models.some((model) => model.value === '')) {
35
+ models.unshift({ value: '', label: 'Runtime default' })
36
+ }
37
+ if (value.value && !models.some((model) => model.value === value.value)) {
38
+ models.unshift({ value: value.value, label: value.value })
39
+ }
40
+ return models
41
+ })
42
+ const label = computed(() => {
43
+ if (value.value) {
44
+ return value.value
45
+ }
46
+ if (runtimeDefault) {
47
+ return 'Default'
48
+ }
49
+ return loading ? 'Loading…' : 'Choose a model'
50
+ })
51
+ const items = computed<DropdownMenuItem[]>(() => {
52
+ const custom = term.value.trim()
53
+ return [
54
+ ...options.value.map((model) => ({
55
+ type: 'checkbox' as const,
56
+ label: model.label,
57
+ description: model.value && model.value !== model.label ? model.value : undefined,
58
+ id: model.value,
59
+ preferred: model.preferred,
60
+ checked: model.value === value.value,
61
+ onSelect: () => (value.value = model.value),
62
+ })),
63
+ ...(custom && !/[\r\n]/.test(custom) && !options.value.some((model) => model.value === custom)
64
+ ? [
65
+ {
66
+ label: `Use ${custom}`,
67
+ icon: 'i-lucide-corner-down-left',
68
+ onSelect: () => (value.value = custom),
69
+ },
70
+ ]
71
+ : []),
72
+ ]
73
+ })
74
+ </script>
75
+
76
+ <template>
77
+ <UDropdownMenu
78
+ v-model:open="isOpen"
79
+ v-model:search-term="term"
80
+ :items="items"
81
+ :filter="{ placeholder: 'Search or enter a model ID', autofocus: true }"
82
+ :filter-fields="['label', 'id']"
83
+ :content="{ align: 'start', sideOffset: 6 }"
84
+ :ui="{ ...MENU_UI, content: `${MENU_UI.content} max-h-80` }"
85
+ >
86
+ <UButton
87
+ :label="label"
88
+ :title="value || 'Runtime default'"
89
+ trailing-icon="i-lucide-chevron-down"
90
+ variant="ghost"
91
+ :class="
92
+ field
93
+ ? 'h-8 w-full justify-between rounded-lg bg-transparent px-2.5 font-normal text-highlighted ring ring-inset ring-(--gray-500) hover:bg-(--gray-400) aria-expanded:bg-(--gray-400)'
94
+ : PILL_CLASS
95
+ "
96
+ :ui="{
97
+ label: value ? 'truncate font-mono text-xs' : 'truncate',
98
+ trailingIcon: 'size-3.5 text-muted',
99
+ }"
100
+ />
101
+ <template #item-trailing="{ item }">
102
+ <span
103
+ v-if="item.preferred"
104
+ class="text-xs text-dimmed"
105
+ >
106
+ CLI default
107
+ </span>
108
+ </template>
109
+ <template #empty>
110
+ <p class="px-2.5 py-2 text-[13px] text-dimmed">No models match.</p>
111
+ </template>
112
+ </UDropdownMenu>
113
+ </template>
@@ -0,0 +1,114 @@
1
+ <script setup lang="ts">
2
+ import { ref, onBeforeUnmount } from 'vue'
3
+ import type { AgentRunResult } from '@cloudconfig/core/agents/runs'
4
+ const { agentId } = defineProps<{ agentId: number }>()
5
+ const instructions = ref('')
6
+ const payload = ref('{}')
7
+ const runResult = ref<AgentRunResult | null>(null)
8
+ const error = ref('')
9
+ const isBusy = ref(false)
10
+ let timer: ReturnType<typeof setTimeout> | undefined
11
+ let isDisposed = false
12
+ onBeforeUnmount(() => {
13
+ isDisposed = true
14
+ if (timer) {
15
+ clearTimeout(timer)
16
+ }
17
+ })
18
+ async function poll() {
19
+ if (!runResult.value || isDisposed) {
20
+ return
21
+ }
22
+ try {
23
+ runResult.value = await $fetch<AgentRunResult>(
24
+ `/api/agents/${agentId}/runs/${runResult.value.runId}`,
25
+ )
26
+ if (isDisposed) {
27
+ return
28
+ }
29
+ if (['queued', 'running', 'waiting'].includes(runResult.value.status)) {
30
+ timer = setTimeout(poll, 2000)
31
+ } else {
32
+ isBusy.value = false
33
+ }
34
+ } catch (cause) {
35
+ error.value = cause instanceof Error ? cause.message : 'Could not load the run.'
36
+ isBusy.value = false
37
+ }
38
+ }
39
+ async function run() {
40
+ error.value = ''
41
+ isBusy.value = true
42
+ try {
43
+ runResult.value = await $fetch<AgentRunResult>(`/api/agents/${agentId}/runs`, {
44
+ method: 'POST',
45
+ body: {
46
+ runId: crypto.randomUUID(),
47
+ instructions: instructions.value,
48
+ payload: JSON.parse(payload.value),
49
+ },
50
+ })
51
+ await poll()
52
+ } catch (cause) {
53
+ error.value = cause instanceof Error ? cause.message : 'Could not run the agent.'
54
+ isBusy.value = false
55
+ }
56
+ }
57
+ </script>
58
+ <template>
59
+ <div class="space-y-4">
60
+ <UFormField label="Instructions">
61
+ <UTextarea
62
+ v-model="instructions"
63
+ placeholder="What should the agent do?"
64
+ class="w-full"
65
+ />
66
+ </UFormField>
67
+ <UFormField label="Input JSON">
68
+ <UTextarea
69
+ v-model="payload"
70
+ class="w-full font-mono"
71
+ />
72
+ </UFormField>
73
+ <UButton
74
+ label="Run saved agent"
75
+ color="neutral"
76
+ :loading="isBusy"
77
+ @click="run"
78
+ />
79
+ <UAlert
80
+ v-if="error"
81
+ color="error"
82
+ :title="error"
83
+ />
84
+ <div
85
+ v-if="runResult"
86
+ class="space-y-2"
87
+ aria-live="polite"
88
+ >
89
+ <UBadge
90
+ color="neutral"
91
+ variant="subtle"
92
+ >
93
+ {{ runResult.status }}
94
+ </UBadge>
95
+ <p
96
+ v-if="runResult.status === 'waiting'"
97
+ class="text-sm text-muted"
98
+ >
99
+ Approve or reject action #{{ runResult.approvalId }} in Integrations. This run resumes after
100
+ your response.
101
+ </p>
102
+ <p
103
+ v-else-if="runResult.error"
104
+ class="text-sm text-error"
105
+ >
106
+ {{ runResult.error }}
107
+ </p>
108
+ <pre
109
+ v-if="runResult.output"
110
+ class="max-h-80 overflow-auto text-sm whitespace-pre-wrap"
111
+ >{{ runResult.output }}</pre>
112
+ </div>
113
+ </div>
114
+ </template>