@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,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>
|