@nuxtagentic/agents 0.0.0-stage → 0.1.0

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,276 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import type { DropdownMenuItem } from '@nuxt/ui'
4
+ import { AGENT_PROVIDERS, type AgentDefinition, type AgentProvider } from '@cloudconfig/core/agents'
5
+ import { useAgentRuntimes } from '../composables/useAgentRuntimes'
6
+ import {
7
+ LOCAL_RUNTIMES,
8
+ PROVIDER_LABELS,
9
+ isLocalRuntime,
10
+ type AgentRuntimeCatalog,
11
+ } from '../shared/runtime'
12
+ import { MENU_UI, PILL_CLASS, PROVIDER_DOTS } from '../shared/presentation'
13
+ import ModelSelect from './ModelSelect.vue'
14
+
15
+ const { catalog } = defineProps<{ catalog?: AgentRuntimeCatalog }>()
16
+ const agent = defineModel<AgentDefinition>({ required: true })
17
+ const runtimes = catalog ? undefined : useAgentRuntimes()
18
+ const runtimeCatalog = computed(() => catalog ?? runtimes?.data.value)
19
+ const error = computed(() => runtimes?.error.value)
20
+ const isLoading = computed(() => runtimes?.status.value === 'pending' && !runtimeCatalog.value)
21
+ function revalidate() {
22
+ runtimes?.revalidate()
23
+ }
24
+ function refresh() {
25
+ return runtimes?.refresh()
26
+ }
27
+ // Deployed hosts always run the production API configuration.
28
+ const isUsingLocalRuntime = computed(
29
+ () => runtimeCatalog.value?.localEnabled !== false && isLocalRuntime(agent.value.runtime),
30
+ )
31
+ const localRuntime = computed(() =>
32
+ isLocalRuntime(agent.value.runtime) ? agent.value.runtime : null,
33
+ )
34
+ const provider = computed(() =>
35
+ isUsingLocalRuntime.value && localRuntime.value
36
+ ? LOCAL_RUNTIMES[localRuntime.value].provider
37
+ : agent.value.provider,
38
+ )
39
+ const runtimeLabel = computed(() =>
40
+ isUsingLocalRuntime.value && localRuntime.value
41
+ ? LOCAL_RUNTIMES[localRuntime.value].label
42
+ : `${PROVIDER_LABELS[agent.value.provider]} API`,
43
+ )
44
+ const apiRuntime = (value: AgentProvider) =>
45
+ runtimeCatalog.value?.api.find((runtime) => runtime.value === value)
46
+ const models = computed(
47
+ () =>
48
+ (isUsingLocalRuntime.value
49
+ ? runtimeCatalog.value?.local.find((runtime) => runtime.value === agent.value.runtime)
50
+ : apiRuntime(agent.value.provider)
51
+ )?.models ?? [],
52
+ )
53
+ const isApiKeyMissing = computed(
54
+ () =>
55
+ !isUsingLocalRuntime.value &&
56
+ !!runtimeCatalog.value &&
57
+ !apiRuntime(agent.value.provider)?.available,
58
+ )
59
+ const runtimeItems = computed<DropdownMenuItem[]>(() => {
60
+ // Like Open Run, list installed CLIs only, keeping a saved choice visible.
61
+ const installed = (runtimeCatalog.value?.local ?? []).filter(
62
+ (runtime) => runtime.available || runtime.value === agent.value.runtime,
63
+ )
64
+ return [
65
+ ...(installed.length
66
+ ? [
67
+ { type: 'label' as const, label: 'Local' },
68
+ ...installed.map((runtime) => ({
69
+ type: 'checkbox' as const,
70
+ label: runtime.label,
71
+ dot: PROVIDER_DOTS[runtime.provider],
72
+ hint: runtime.available ? '' : 'Not installed',
73
+ checked: isUsingLocalRuntime.value && agent.value.runtime === runtime.value,
74
+ onSelect: () => setRuntime(runtime.value),
75
+ })),
76
+ { type: 'separator' as const },
77
+ ]
78
+ : []),
79
+ { type: 'label' as const, label: 'API' },
80
+ ...AGENT_PROVIDERS.map((value) => ({
81
+ type: 'checkbox' as const,
82
+ label: `${PROVIDER_LABELS[value]} API`,
83
+ dot: PROVIDER_DOTS[value],
84
+ hint: runtimeCatalog.value && !apiRuntime(value)?.available ? 'No key' : '',
85
+ checked: !isUsingLocalRuntime.value && agent.value.provider === value,
86
+ onSelect: () => setRuntime(value),
87
+ })),
88
+ ]
89
+ })
90
+ const productionItems = computed<DropdownMenuItem[]>(() =>
91
+ AGENT_PROVIDERS.map((value) => ({
92
+ type: 'checkbox' as const,
93
+ label: `${PROVIDER_LABELS[value]} API`,
94
+ dot: PROVIDER_DOTS[value],
95
+ hint: runtimeCatalog.value && !apiRuntime(value)?.available ? 'No key' : '',
96
+ checked: agent.value.provider === value,
97
+ onSelect: () => setProvider(value),
98
+ })),
99
+ )
100
+ const modelValue = computed({
101
+ get: () => (isUsingLocalRuntime.value ? (agent.value.localModel ?? '') : agent.value.model),
102
+ set: (value: string) => {
103
+ agent.value = {
104
+ ...agent.value,
105
+ ...(isUsingLocalRuntime.value ? { localModel: value } : { model: value }),
106
+ }
107
+ },
108
+ })
109
+ const productionModel = computed({
110
+ get: () => agent.value.model,
111
+ set: (model: string) => (agent.value = { ...agent.value, model }),
112
+ })
113
+ function setProvider(value: AgentProvider) {
114
+ if (agent.value.provider === value) {
115
+ return
116
+ }
117
+ agent.value = {
118
+ ...agent.value,
119
+ provider: value,
120
+ model: apiRuntime(value)?.models[0]?.value ?? '',
121
+ }
122
+ }
123
+ function setRuntime(value: string) {
124
+ if (isLocalRuntime(value)) {
125
+ agent.value = {
126
+ ...agent.value,
127
+ runtime: value,
128
+ localModel: agent.value.runtime === value ? agent.value.localModel : '',
129
+ }
130
+ return
131
+ }
132
+ setProvider(value as AgentProvider)
133
+ agent.value = { ...agent.value, runtime: 'api' }
134
+ }
135
+ </script>
136
+
137
+ <template>
138
+ <UDropdownMenu
139
+ :items="runtimeItems"
140
+ :content="{ align: 'start', sideOffset: 6 }"
141
+ :ui="MENU_UI"
142
+ @update:open="$event && revalidate()"
143
+ >
144
+ <UButton
145
+ :label="runtimeLabel"
146
+ trailing-icon="i-lucide-chevron-down"
147
+ variant="ghost"
148
+ :class="PILL_CLASS"
149
+ :ui="{ label: 'truncate', trailingIcon: 'size-3.5 text-muted' }"
150
+ >
151
+ <template #leading>
152
+ <span
153
+ aria-hidden="true"
154
+ class="size-1.5 shrink-0 rounded-full"
155
+ :class="PROVIDER_DOTS[provider]"
156
+ />
157
+ </template>
158
+ </UButton>
159
+ <template #item-leading="{ item }">
160
+ <span
161
+ aria-hidden="true"
162
+ class="size-1.5 shrink-0 rounded-full"
163
+ :class="item.dot"
164
+ />
165
+ </template>
166
+ <template #item-trailing="{ item }">
167
+ <span
168
+ v-if="item.hint"
169
+ class="text-xs text-dimmed"
170
+ >
171
+ {{ item.hint }}
172
+ </span>
173
+ </template>
174
+ <template #content-bottom>
175
+ <div
176
+ v-if="error"
177
+ class="mt-1 flex items-center gap-2 rounded-lg bg-(--gray-400) px-2.5 py-2"
178
+ >
179
+ <p
180
+ role="alert"
181
+ class="flex-1 text-xs text-error"
182
+ >
183
+ Could not load runtimes.
184
+ </p>
185
+ <UButton
186
+ label="Retry"
187
+ size="xs"
188
+ @click="refresh()"
189
+ />
190
+ </div>
191
+ <p
192
+ v-else-if="isApiKeyMissing"
193
+ class="mt-1 flex items-center gap-2 rounded-lg bg-(--gray-400) px-2.5 py-2 text-xs text-toned"
194
+ >
195
+ <span class="size-1.5 shrink-0 rounded-full bg-warning" />
196
+ {{ apiRuntime(agent.provider)?.hint }}
197
+ </p>
198
+ </template>
199
+ </UDropdownMenu>
200
+ <ModelSelect
201
+ v-model="modelValue"
202
+ :models="models"
203
+ :loading="isLoading"
204
+ :runtime-default="isUsingLocalRuntime"
205
+ @open="revalidate"
206
+ />
207
+ <slot />
208
+ <UPopover
209
+ v-if="isUsingLocalRuntime"
210
+ :content="{ align: 'start', sideOffset: 6 }"
211
+ :ui="{ content: 'w-72 rounded-[10px] p-3 ring-(--gray-500)' }"
212
+ >
213
+ <UButton
214
+ icon="i-lucide-cloud"
215
+ aria-label="Production API configuration"
216
+ title="Production API configuration"
217
+ variant="ghost"
218
+ square
219
+ class="size-7.5 rounded-full aria-expanded:bg-(--gray-400) aria-expanded:text-highlighted hover:bg-(--gray-400)"
220
+ />
221
+ <template #content>
222
+ <div class="space-y-3">
223
+ <p class="text-xs text-muted">
224
+ Deployed hosts run this API configuration instead of the local CLI.
225
+ </p>
226
+ <UFormField label="Provider">
227
+ <UDropdownMenu
228
+ :items="productionItems"
229
+ :content="{ align: 'start', sideOffset: 6 }"
230
+ :ui="MENU_UI"
231
+ >
232
+ <UButton
233
+ :label="`${PROVIDER_LABELS[agent.provider]} API`"
234
+ trailing-icon="i-lucide-chevron-down"
235
+ variant="ghost"
236
+ class="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)"
237
+ :ui="{ trailingIcon: 'size-3.5 text-muted' }"
238
+ >
239
+ <template #leading>
240
+ <span
241
+ aria-hidden="true"
242
+ class="size-1.5 shrink-0 rounded-full"
243
+ :class="PROVIDER_DOTS[agent.provider]"
244
+ />
245
+ </template>
246
+ </UButton>
247
+ <template #item-leading="{ item }">
248
+ <span
249
+ aria-hidden="true"
250
+ class="size-1.5 shrink-0 rounded-full"
251
+ :class="item.dot"
252
+ />
253
+ </template>
254
+ <template #item-trailing="{ item }">
255
+ <span
256
+ v-if="item.hint"
257
+ class="text-xs text-dimmed"
258
+ >
259
+ {{ item.hint }}
260
+ </span>
261
+ </template>
262
+ </UDropdownMenu>
263
+ </UFormField>
264
+ <UFormField label="Model">
265
+ <ModelSelect
266
+ v-model="productionModel"
267
+ :models="apiRuntime(agent.provider)?.models ?? []"
268
+ :loading="isLoading"
269
+ field
270
+ @open="revalidate"
271
+ />
272
+ </UFormField>
273
+ </div>
274
+ </template>
275
+ </UPopover>
276
+ </template>
@@ -0,0 +1,78 @@
1
+ <script setup lang="ts">
2
+ import { onBeforeUnmount, onMounted, useTemplateRef, watch } from 'vue'
3
+ import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'
4
+ import { markdown } from '@codemirror/lang-markdown'
5
+ import { yamlFrontmatter } from '@codemirror/lang-yaml'
6
+ import { EditorState } from '@codemirror/state'
7
+ import { EditorView, keymap } from '@codemirror/view'
8
+ import { sourceEditorTheme, sourceSyntaxHighlighting } from '../shared/editorTheme'
9
+
10
+ const { modelValue, label } = defineProps<{ modelValue: string; label: string }>()
11
+ const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
12
+ const root = useTemplateRef('root')
13
+ let view: EditorView | undefined
14
+
15
+ onMounted(() => {
16
+ view = new EditorView({
17
+ parent: root.value!,
18
+ state: EditorState.create({
19
+ doc: modelValue,
20
+ extensions: [
21
+ history(),
22
+ // Mod-Enter saves the agent.
23
+ keymap.of([
24
+ ...defaultKeymap.filter((binding) => binding.key !== 'Mod-Enter'),
25
+ ...historyKeymap,
26
+ ]),
27
+ yamlFrontmatter({ content: markdown() }),
28
+ sourceEditorTheme,
29
+ sourceSyntaxHighlighting,
30
+ EditorView.lineWrapping,
31
+ EditorView.contentAttributes.of({ 'aria-label': label, spellcheck: 'false' }),
32
+ EditorView.updateListener.of((update) => {
33
+ if (!update.docChanged) {
34
+ return
35
+ }
36
+ const value = update.state.doc.toString()
37
+ if (value !== modelValue) {
38
+ emit('update:modelValue', value)
39
+ }
40
+ }),
41
+ ],
42
+ }),
43
+ })
44
+ })
45
+ onBeforeUnmount(() => view?.destroy())
46
+
47
+ // Replace only the changed span so the caret stays put when settings rewrite the frontmatter.
48
+ watch(
49
+ () => modelValue,
50
+ (value) => {
51
+ const current = view?.state.doc.toString()
52
+ if (!view || current === undefined || value === current) {
53
+ return
54
+ }
55
+ let from = 0
56
+ while (from < current.length && from < value.length && current[from] === value[from]) {
57
+ from++
58
+ }
59
+ let end = 0
60
+ while (
61
+ end < current.length - from &&
62
+ end < value.length - from &&
63
+ current[current.length - 1 - end] === value[value.length - 1 - end]
64
+ ) {
65
+ end++
66
+ }
67
+ view.dispatch({
68
+ changes: { from, to: current.length - end, insert: value.slice(from, value.length - end) },
69
+ })
70
+ },
71
+ )
72
+
73
+ defineExpose({ focus: () => view?.focus() })
74
+ </script>
75
+
76
+ <template>
77
+ <div ref="root" />
78
+ </template>
@@ -0,0 +1,24 @@
1
+ import { useAsyncData, useRequestFetch } from '#imports'
2
+ import { useWorkspaceModules } from '@nuxtagentic/workspace/runtime/composables/useWorkspaceModules'
3
+ import type { AgentIntegrationTool } from '../shared/integration-tool'
4
+
5
+ export function useAgentIntegrationTools() {
6
+ const modules = useWorkspaceModules()
7
+ const fetcher = useRequestFetch()
8
+
9
+ return useAsyncData(
10
+ 'agents:integration-tools',
11
+ () =>
12
+ modules.isInstalled('integrations')
13
+ ? fetcher<AgentIntegrationTool[]>('/api/integrations/catalog/tools')
14
+ : Promise.resolve([]),
15
+ {
16
+ server: false,
17
+ lazy: true,
18
+ deep: false,
19
+ dedupe: 'defer',
20
+ getCachedData: (key, nuxtApp, { cause }) =>
21
+ cause === 'initial' ? nuxtApp.payload.data[key] : undefined,
22
+ },
23
+ )
24
+ }
@@ -0,0 +1,32 @@
1
+ import { useAsyncData, useRequestFetch } from '#imports'
2
+ import type { AgentRuntimeCatalog } from '../shared/runtime'
3
+
4
+ const STALE_AFTER = 60_000
5
+
6
+ /** One runtime catalog per session; editors reuse it instead of refetching on mount. */
7
+ export function useAgentRuntimes() {
8
+ const fetcher = useRequestFetch()
9
+ const catalog = useAsyncData(
10
+ 'agents:runtimes',
11
+ async () => ({
12
+ ...(await fetcher<AgentRuntimeCatalog>('/api/agents/runtimes')),
13
+ loadedAt: Date.now(),
14
+ }),
15
+ {
16
+ server: false,
17
+ lazy: true,
18
+ deep: false,
19
+ dedupe: 'defer',
20
+ getCachedData: (key, nuxtApp, { cause }) =>
21
+ cause === 'initial' ? nuxtApp.payload.data[key] : undefined,
22
+ },
23
+ )
24
+ /** Picks up a newly installed CLI or API key when a menu opens. */
25
+ function revalidate() {
26
+ const loadedAt = catalog.data.value?.loadedAt ?? 0
27
+ if (catalog.status.value !== 'pending' && Date.now() - loadedAt > STALE_AFTER) {
28
+ void catalog.refresh()
29
+ }
30
+ }
31
+ return { ...catalog, revalidate }
32
+ }
@@ -0,0 +1,24 @@
1
+ import { computed, type Ref } from 'vue'
2
+ import { useCloudConfig } from '#imports'
3
+ import type { AgentsConfig } from '@cloudconfig/core/agents'
4
+ import { useWorkspaceQuery } from '@nuxtagentic/data/runtime/composables/useWorkspaceQuery'
5
+
6
+ export { toAgentDefinition, type AgentRecord } from '../shared/definition'
7
+
8
+ export function useAgents(query: Ref<{ search?: string; page?: number }> = computed(() => ({}))) {
9
+ const api = useCloudConfig<AgentsConfig>().agent
10
+ const resource = useWorkspaceQuery('agents', query, (query, signal) =>
11
+ api.list({ ...query, pageSize: 50, sortBy: 'name', sortDir: 'asc' }, { signal }),
12
+ )
13
+ const meta = computed(() => {
14
+ if (resource.error.value) {
15
+ return 'Unavailable'
16
+ }
17
+ if (resource.status.value !== 'success') {
18
+ return 'Loading…'
19
+ }
20
+ const count = resource.totalCount.value
21
+ return `${count} ${count === 1 ? 'agent' : 'agents'}`
22
+ })
23
+ return { ...resource, api, meta }
24
+ }
@@ -0,0 +1,71 @@
1
+ import { computed } from 'vue'
2
+ import type { WorkspaceFeature } from '@nuxtagentic/workspace/runtime/types'
3
+ import AgentsWorkspace from './components/AgentsWorkspace.vue'
4
+ import { useAgents } from './composables/useAgents'
5
+ import { createAgentsPromptParser } from './shared/prompt'
6
+ import { actions } from './actions'
7
+
8
+ export const agentsFeature = {
9
+ id: 'agents',
10
+ label: 'Agents',
11
+ icon: 'i-lucide-bot',
12
+ matchesRoute: (route) => route.path === '/dashboard/agents',
13
+ collectionLocation: (query) => ({
14
+ path: '/dashboard/agents',
15
+ query: {
16
+ ...query,
17
+ module: undefined,
18
+ agentSearch:
19
+ query.agentSearch ??
20
+ (typeof query.q === 'string' && /^agents?\b/i.test(query.q)
21
+ ? query.q.replace(/^agents?\s*/i, '') || undefined
22
+ : undefined),
23
+ },
24
+ }),
25
+ recordLocation: (id, query) => ({
26
+ path: '/dashboard/agents',
27
+ query: { project: query.project, agent: String(id) },
28
+ }),
29
+ navigation: { order: 10 },
30
+ layout: { navbar: true, expand: true, edgeToEdge: true, ownsDetailAside: true, hideExpand: true },
31
+ prompt: { query: 'agents ', useMeta: () => useAgents().meta },
32
+ urlKeys: ['agent', 'agentSearch', 'agentPage'],
33
+ selectionKey: 'agent',
34
+ draftKeys: ['agentName'],
35
+ backKeys: ['agentSearch', 'agentPage'],
36
+ component: AgentsWorkspace,
37
+ actions,
38
+ createPromptParser: createAgentsPromptParser,
39
+ reviewIntent: (intent) => ({
40
+ module: 'agents',
41
+ agent: 'new',
42
+ agentName: 'name' in intent && typeof intent.name === 'string' ? intent.name : undefined,
43
+ }),
44
+ useSearch({ text }) {
45
+ const agentLink = (extra = {}) => ({
46
+ path: '/dashboard/agents',
47
+ query: { agentSearch: text.value, ...extra },
48
+ })
49
+ const agents = useAgents(computed(() => ({ search: text.value })))
50
+ return {
51
+ featureId: 'agents',
52
+ meta: agents.meta,
53
+ refresh: agents.refreshAll,
54
+ group: computed(() => ({
55
+ count: text.value ? agents.totalCount.value : 0,
56
+ to: agentLink(),
57
+ loading: agents.isLoading.value,
58
+ error: agents.error.value?.message ?? '',
59
+ items: text.value
60
+ ? agents.rows.value.slice(0, 3).map((agent) => ({
61
+ id: agent.id,
62
+ label: agent.name,
63
+ description: agent.model,
64
+ icon: 'i-lucide-bot',
65
+ to: agentLink({ agent: String(agent.id) }),
66
+ }))
67
+ : [],
68
+ })),
69
+ }
70
+ },
71
+ } satisfies WorkspaceFeature
@@ -0,0 +1,10 @@
1
+ import { defineNuxtRouteMiddleware, navigateTo } from '#imports'
2
+
3
+ export default defineNuxtRouteMiddleware((to) => {
4
+ if (to.path === '/dashboard' && (to.query.module === 'agents' || to.query.agent)) {
5
+ return navigateTo(
6
+ { path: '/dashboard/agents', query: { ...to.query, module: undefined } },
7
+ { replace: true },
8
+ )
9
+ }
10
+ })
@@ -0,0 +1,26 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { definePageMeta, useHead } from '#imports'
4
+ import { useWorkspaceContext } from '@nuxtagentic/workspace/runtime/composables/useWorkspaceContext'
5
+ import { useWorkspaceRoute } from '@nuxtagentic/workspace/runtime/composables/useWorkspaceRoute'
6
+ import type { WorkspaceEntryContext } from '@nuxtagentic/workspace/runtime/types'
7
+ import AgentsWorkspace from '../components/AgentsWorkspace.vue'
8
+
9
+ definePageMeta({ layout: 'dashboard', middleware: ['auth'], title: 'Agents' })
10
+ useHead({ title: 'Agents · Agent UI' })
11
+
12
+ const workspace = useWorkspaceContext()
13
+ const navigation = useWorkspaceRoute()
14
+ // Page route state is available before the persistent layout finishes navigation.
15
+ const context: WorkspaceEntryContext = { ...workspace.entryContext, navigation }
16
+ const isActive = computed(() => workspace.activeFeature.value?.id === 'agents')
17
+ </script>
18
+
19
+ <template>
20
+ <div class="min-h-0 flex-1 overflow-hidden">
21
+ <AgentsWorkspace
22
+ :context="context"
23
+ :active="isActive"
24
+ />
25
+ </div>
26
+ </template>
@@ -0,0 +1,11 @@
1
+ import { defineEventHandler, getRouterParam, createError } from 'h3'
2
+ import { connectAgentGateway } from '../../../../utils/agent-gateway'
3
+
4
+ export default defineEventHandler(async (event) => {
5
+ const gateway = await connectAgentGateway(event, Number(getRouterParam(event, 'id')))
6
+ const result = await gateway.runtime.status(String(getRouterParam(event, 'run')))
7
+ if (!result) {
8
+ throw createError({ statusCode: 404, statusMessage: 'Agent run not found.' })
9
+ }
10
+ return result
11
+ })
@@ -0,0 +1,33 @@
1
+ import { defineEventHandler, getRouterParam, getHeader, getRequestURL, createError } from 'h3'
2
+ import { readCommandBody } from '@nuxtagentic/integrations/runtime/server/utils/request-body'
3
+ import { connectAgentGateway } from '../../../utils/agent-gateway'
4
+ import { isLocalAgentRuntimeEnabled } from '../../../utils/local-agent-runtime'
5
+
6
+ export default defineEventHandler(async (event) => {
7
+ if (getHeader(event, 'origin') !== getRequestURL(event).origin) {
8
+ throw createError({ statusCode: 403, statusMessage: 'Run agents from this application.' })
9
+ }
10
+ const body = await readCommandBody<{ runId?: string; payload?: unknown; instructions?: string }>(
11
+ event,
12
+ 64_000,
13
+ )
14
+ const runId = body.runId ?? crypto.randomUUID()
15
+ if (
16
+ !/^[a-zA-Z0-9_-]{1,100}$/.test(runId) ||
17
+ typeof body.instructions !== 'string' ||
18
+ body.instructions.length > 20_000
19
+ ) {
20
+ throw createError({
21
+ statusCode: 400,
22
+ statusMessage: 'Use a valid run ID and instructions of at most 20,000 characters.',
23
+ })
24
+ }
25
+ const gateway = await connectAgentGateway(event, Number(getRouterParam(event, 'id')))
26
+ return gateway.runtime.start({
27
+ runId,
28
+ scope: gateway.scope,
29
+ payload: body.payload ?? {},
30
+ instructions: body.instructions,
31
+ context: { localCli: isLocalAgentRuntimeEnabled(event) },
32
+ })
33
+ })
@@ -0,0 +1,37 @@
1
+ import { createError, defineEventHandler } from 'h3'
2
+ import { AGENT_PROVIDERS } from '@cloudconfig/core/agents'
3
+ import { useAgentSession } from '@nuxtagentic/prompt/runtime/server/utils/agent-session'
4
+ import { PROVIDER_LABELS, type AgentRuntimeCatalog } from '../../../shared/runtime'
5
+ import { getAgentProviderKeysForEvent } from '../../utils/agent-provider-keys'
6
+ import { listApiAgentModels } from '../../utils/agent-model-catalog'
7
+ import { isLocalAgentRuntimeEnabled } from '../../utils/local-agent-runtime'
8
+
9
+ export default defineEventHandler(async (event): Promise<AgentRuntimeCatalog> => {
10
+ if (!(await useAgentSession(event))?.user) {
11
+ throw createError({ statusCode: 401, statusMessage: 'Sign in to choose an agent runtime.' })
12
+ }
13
+ const keys = getAgentProviderKeysForEvent(event)
14
+ const localEnabled = isLocalAgentRuntimeEnabled(event)
15
+ const [local, api] = await Promise.all([
16
+ localEnabled
17
+ ? import('../../utils/local-agent-catalog').then((module) => module.getLocalAgentCatalog())
18
+ : [],
19
+ Promise.all(
20
+ AGENT_PROVIDERS.map(async (value) => {
21
+ let hint = `Set the ${PROVIDER_LABELS[value]} API key on the server to run this agent.`
22
+ if (keys[value]) {
23
+ hint = 'API key configured on the server.'
24
+ }
25
+ return {
26
+ value,
27
+ label: `${PROVIDER_LABELS[value]} API`,
28
+ provider: value,
29
+ available: !!keys[value],
30
+ hint,
31
+ models: await listApiAgentModels(value, keys[value]),
32
+ }
33
+ }),
34
+ ),
35
+ ])
36
+ return { localEnabled, local, api }
37
+ })