@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.
- package/LICENSE.md +9 -0
- package/README.md +99 -2
- package/module.ts +35 -0
- package/package.json +56 -3
- package/runtime/actions.ts +22 -0
- package/runtime/components/AgentEditor.vue +446 -0
- package/runtime/components/AgentList.vue +110 -0
- package/runtime/components/AgentToolPicker.vue +371 -0
- package/runtime/components/AgentsWorkspace.vue +328 -0
- package/runtime/components/ModelSelect.vue +113 -0
- package/runtime/components/RunPanel.vue +114 -0
- package/runtime/components/RuntimeModelPicker.vue +276 -0
- package/runtime/components/SourceEditor.vue +78 -0
- package/runtime/composables/useAgentIntegrationTools.ts +24 -0
- package/runtime/composables/useAgentRuntimes.ts +32 -0
- package/runtime/composables/useAgents.ts +24 -0
- package/runtime/index.ts +71 -0
- package/runtime/middleware/agents-route.ts +10 -0
- package/runtime/pages/agents.vue +26 -0
- package/runtime/server/api/agents/[id]/runs/[run].get.ts +11 -0
- package/runtime/server/api/agents/[id]/runs.post.ts +33 -0
- package/runtime/server/api/agents/runtimes.get.ts +37 -0
- package/runtime/server/utils/agent-execution.ts +124 -0
- package/runtime/server/utils/agent-gateway.ts +13 -0
- package/runtime/server/utils/agent-model-catalog.ts +93 -0
- package/runtime/server/utils/agent-provider-keys.ts +46 -0
- package/runtime/server/utils/local-agent-catalog.ts +151 -0
- package/runtime/server/utils/local-agent-runtime.ts +8 -0
- package/runtime/server/utils/model-fetch.ts +66 -0
- package/runtime/server/utils/run-local-agent.ts +208 -0
- package/runtime/server/utils/tool-result.ts +22 -0
- package/runtime/shared/agent-summary.ts +9 -0
- package/runtime/shared/agent.ts +4 -0
- package/runtime/shared/definition.ts +16 -0
- package/runtime/shared/editorTheme.ts +43 -0
- package/runtime/shared/integration-tool.ts +11 -0
- package/runtime/shared/presentation.ts +50 -0
- package/runtime/shared/prompt.ts +37 -0
- package/runtime/shared/runtime.ts +40 -0
- package/runtime/shared/source.ts +81 -0
- package/schema.ts +2 -0
- 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
|
+
}
|
package/runtime/index.ts
ADDED
|
@@ -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
|
+
})
|