chore: kanban template
This commit is contained in:
161
src/scaffold/agent.gen.ts
Normal file
161
src/scaffold/agent.gen.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.23
|
||||
*/
|
||||
import { MissingServiceError } from '@pikku/core/errors'
|
||||
import { pikkuSessionlessFunc, defineHTTPRoutes, wireHTTPRoutes } from '@project/functions/.pikku/pikku-types.gen.js'
|
||||
|
||||
export const agentCaller = pikkuSessionlessFunc<
|
||||
{ agentName: string; message: string; threadId: string; resourceId: string },
|
||||
unknown
|
||||
>({
|
||||
tags: ['pikku'],
|
||||
auth: false,
|
||||
func: async (_services, data, { rpc }) => {
|
||||
return await rpc.agent.run(data.agentName as any, {
|
||||
message: data.message,
|
||||
threadId: data.threadId,
|
||||
resourceId: data.resourceId,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const agentStreamCaller = pikkuSessionlessFunc<
|
||||
{ agentName: string; message: string; threadId: string; resourceId: string },
|
||||
void
|
||||
>({
|
||||
tags: ['pikku'],
|
||||
auth: false,
|
||||
func: async (_services, data, { rpc }) => {
|
||||
await rpc.agent.stream(data.agentName as any, {
|
||||
message: data.message,
|
||||
threadId: data.threadId,
|
||||
resourceId: data.resourceId,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const agentApproveCaller = pikkuSessionlessFunc<
|
||||
{ agentName: string; runId: string; approvals: { toolCallId: string; approved: boolean }[] },
|
||||
unknown
|
||||
>({
|
||||
tags: ['pikku'],
|
||||
auth: false,
|
||||
func: async (_services, { runId, approvals, agentName }, { rpc }) => {
|
||||
return await rpc.agent.approve(runId, approvals, agentName)
|
||||
},
|
||||
})
|
||||
|
||||
export const agentResumeCaller = pikkuSessionlessFunc<
|
||||
{ agentName: string; runId: string; toolCallId: string; approved: boolean },
|
||||
void
|
||||
>({
|
||||
tags: ['pikku'],
|
||||
auth: false,
|
||||
func: async (_services, data, { rpc }) => {
|
||||
await rpc.agent.resume(data.runId, {
|
||||
toolCallId: data.toolCallId,
|
||||
approved: data.approved,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const getAgentThreads = pikkuSessionlessFunc<
|
||||
{ agentName?: string; resourceId?: string; limit?: number; offset?: number },
|
||||
any[]
|
||||
>({
|
||||
tags: ['pikku', 'pikku:agent'],
|
||||
title: 'Get Agent Threads',
|
||||
description:
|
||||
'Returns a list of AI agent threads from storage. Accepts optional filters: agentName, resourceId, limit, and offset for pagination.',
|
||||
expose: true,
|
||||
auth: false,
|
||||
func: async ({ agentRunService }, input) => {
|
||||
if (!agentRunService) throw new MissingServiceError('agentRunService is not available')
|
||||
return await agentRunService.listThreads({
|
||||
agentName: input?.agentName,
|
||||
resourceId: input?.resourceId,
|
||||
limit: input?.limit,
|
||||
offset: input?.offset,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const getAgentThreadMessages = pikkuSessionlessFunc<
|
||||
{ threadId: string; resourceId?: string },
|
||||
any[]
|
||||
>({
|
||||
tags: ['pikku', 'pikku:agent'],
|
||||
title: 'Get Agent Thread Messages',
|
||||
description:
|
||||
'Returns all messages for a given AI agent thread, ordered by creation time.',
|
||||
expose: true,
|
||||
auth: false,
|
||||
func: async ({ agentRunService }, input) => {
|
||||
if (!agentRunService) throw new MissingServiceError('agentRunService is not available')
|
||||
return await agentRunService.getThreadMessages(input.threadId)
|
||||
},
|
||||
})
|
||||
|
||||
export const getAgentThreadRuns = pikkuSessionlessFunc<
|
||||
{ threadId: string; resourceId?: string },
|
||||
any[]
|
||||
>({
|
||||
tags: ['pikku', 'pikku:agent'],
|
||||
title: 'Get Agent Thread Runs',
|
||||
description:
|
||||
'Returns the run history for a given AI agent thread, ordered by creation time.',
|
||||
expose: true,
|
||||
auth: false,
|
||||
func: async ({ agentRunService }, input) => {
|
||||
if (!agentRunService) throw new MissingServiceError('agentRunService is not available')
|
||||
return await agentRunService.getThreadRuns(input.threadId)
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteAgentThread = pikkuSessionlessFunc<
|
||||
{ threadId: string; resourceId?: string },
|
||||
{ deleted: boolean }
|
||||
>({
|
||||
tags: ['pikku', 'pikku:agent'],
|
||||
title: 'Delete Agent Thread',
|
||||
description:
|
||||
'Deletes an AI agent thread and all of its persisted state.',
|
||||
expose: true,
|
||||
auth: false,
|
||||
func: async ({ agentRunService }, input) => {
|
||||
if (!agentRunService) throw new MissingServiceError('agentRunService is not available')
|
||||
const deleted = await agentRunService.deleteThread(input.threadId)
|
||||
return { deleted }
|
||||
},
|
||||
})
|
||||
|
||||
export const agentRoutes = defineHTTPRoutes({
|
||||
auth: false,
|
||||
tags: ['pikku:public'],
|
||||
routes: {
|
||||
agentRun: {
|
||||
route: '/rpc/agent/:agentName',
|
||||
method: 'post',
|
||||
func: agentCaller,
|
||||
},
|
||||
agentStream: {
|
||||
route: '/rpc/agent/:agentName/stream',
|
||||
method: 'post',
|
||||
sse: true,
|
||||
func: agentStreamCaller,
|
||||
},
|
||||
agentApprove: {
|
||||
route: '/rpc/agent/:agentName/approve',
|
||||
method: 'post',
|
||||
func: agentApproveCaller,
|
||||
},
|
||||
agentResume: {
|
||||
route: '/rpc/agent/:agentName/resume',
|
||||
method: 'post',
|
||||
sse: true,
|
||||
func: agentResumeCaller,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
wireHTTPRoutes({ routes: { agent: agentRoutes } })
|
||||
108
src/scaffold/console.gen.ts
Normal file
108
src/scaffold/console.gen.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.23
|
||||
*/
|
||||
import { pikkuSessionlessFunc, defineHTTPRoutes, wireHTTPRoutes, ref, wireAddon } from '@project/functions/.pikku/pikku-types.gen.js'
|
||||
|
||||
export const pikkuConsoleSetSecret = pikkuSessionlessFunc<{
|
||||
secretId: string
|
||||
value: unknown
|
||||
}, {
|
||||
success: boolean
|
||||
}>({
|
||||
tags: ['pikku'],
|
||||
description: 'Set the value of a secret',
|
||||
expose: true,
|
||||
auth: false,
|
||||
func: async ({ secrets }, { secretId, value }) => {
|
||||
await secrets.setSecretJSON(secretId, value)
|
||||
return { success: true }
|
||||
},
|
||||
})
|
||||
|
||||
export const pikkuConsoleGetVariable = pikkuSessionlessFunc<
|
||||
{ variableId: string },
|
||||
{ exists: boolean; value: unknown | null }
|
||||
>({
|
||||
tags: ['pikku'],
|
||||
description: 'Get the current value of a variable',
|
||||
expose: true,
|
||||
auth: false,
|
||||
func: async ({ variables }, { variableId }) => {
|
||||
const exists = await variables.has(variableId)
|
||||
if (!exists) {
|
||||
return { exists: false, value: null }
|
||||
}
|
||||
try {
|
||||
const value = await variables.getJSON(variableId)
|
||||
return { exists: true, value }
|
||||
} catch {
|
||||
const value = await variables.get(variableId)
|
||||
return { exists: true, value }
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const pikkuConsoleSetVariable = pikkuSessionlessFunc<
|
||||
{ variableId: string; value: unknown },
|
||||
{ success: boolean }
|
||||
>({
|
||||
tags: ['pikku'],
|
||||
description: 'Set the value of a variable',
|
||||
expose: true,
|
||||
auth: false,
|
||||
func: async ({ variables }, { variableId, value }) => {
|
||||
if (typeof value === 'string') {
|
||||
await variables.set(variableId, value)
|
||||
} else {
|
||||
await variables.setJSON(variableId, value)
|
||||
}
|
||||
return { success: true }
|
||||
},
|
||||
})
|
||||
|
||||
export const pikkuConsoleHasSecret = pikkuSessionlessFunc<
|
||||
{ secretId: string },
|
||||
{ exists: boolean }
|
||||
>({
|
||||
tags: ['pikku'],
|
||||
description: 'Check if a secret exists without reading its value',
|
||||
expose: true,
|
||||
auth: false,
|
||||
func: async ({ secrets }, { secretId }) => {
|
||||
const exists = await secrets.hasSecret(secretId)
|
||||
return { exists }
|
||||
},
|
||||
})
|
||||
|
||||
export const pikkuConsoleGetSecret = pikkuSessionlessFunc<
|
||||
{ secretId: string },
|
||||
{ exists: boolean; value: unknown | null }
|
||||
>({
|
||||
tags: ['pikku'],
|
||||
description: 'Get the current value of a secret',
|
||||
expose: true,
|
||||
auth: false,
|
||||
func: async ({ secrets }, { secretId }) => {
|
||||
const exists = await secrets.hasSecret(secretId)
|
||||
if (!exists) {
|
||||
return { exists: false, value: null }
|
||||
}
|
||||
const value = await secrets.getSecretJSON(secretId)
|
||||
return { exists: true, value }
|
||||
},
|
||||
})
|
||||
|
||||
export const consoleRoutes = defineHTTPRoutes({
|
||||
auth: false,
|
||||
routes: {
|
||||
workflowRunStream: {
|
||||
route: '/workflow-run/:runId/stream',
|
||||
method: 'get',
|
||||
sse: true,
|
||||
func: ref('console:streamWorkflowRun'),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
wireAddon({ name: 'console', package: '@pikku/addon-console' })
|
||||
wireHTTPRoutes({ basePath: '', routes: { console: consoleRoutes } })
|
||||
26
src/scaffold/rpc-public.gen.ts
Normal file
26
src/scaffold/rpc-public.gen.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.23
|
||||
*/
|
||||
/**
|
||||
* Auto-generated public RPC HTTP endpoint
|
||||
* Do not edit manually - regenerate with 'npx pikku'
|
||||
*/
|
||||
import { pikkuSessionlessFunc, wireHTTP } from '@project/functions/.pikku/pikku-types.gen.js'
|
||||
|
||||
export const rpcCaller = pikkuSessionlessFunc<
|
||||
{ rpcName: string; data?: unknown },
|
||||
unknown
|
||||
>({
|
||||
tags: ['pikku'],
|
||||
auth: false,
|
||||
func: async (_services, { rpcName, data }, { rpc }) => {
|
||||
return await rpc.exposed(rpcName, data)
|
||||
},
|
||||
})
|
||||
|
||||
wireHTTP({
|
||||
route: '/rpc/:rpcName',
|
||||
method: 'post',
|
||||
auth: false,
|
||||
func: rpcCaller,
|
||||
})
|
||||
33
src/scaffold/rpc-remote.gen.ts
Normal file
33
src/scaffold/rpc-remote.gen.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.23
|
||||
*/
|
||||
/**
|
||||
* Auto-generated remote internal RPC queue worker and HTTP endpoint
|
||||
* Do not edit manually - regenerate with 'npx pikku'
|
||||
*/
|
||||
import { pikkuSessionlessFunc, wireHTTP, wireQueueWorker } from '@project/functions/.pikku/pikku-types.gen.js'
|
||||
import { pikkuRemoteAuthMiddleware } from '@pikku/core/middleware'
|
||||
|
||||
export const remoteRPCHandler = pikkuSessionlessFunc<
|
||||
{ rpcName: string, data?: unknown },
|
||||
unknown
|
||||
>({
|
||||
tags: ['pikku'],
|
||||
func: async (_services, { rpcName, data }, { rpc }) => {
|
||||
return await (rpc.invoke as any)(rpcName, data)
|
||||
},
|
||||
remote: true,
|
||||
})
|
||||
|
||||
wireQueueWorker({
|
||||
name: 'pikku-remote-internal-rpc',
|
||||
func: remoteRPCHandler,
|
||||
})
|
||||
|
||||
wireHTTP({
|
||||
route: '/remote/rpc/:rpcName',
|
||||
method: 'post',
|
||||
auth: false,
|
||||
middleware: [pikkuRemoteAuthMiddleware],
|
||||
func: remoteRPCHandler,
|
||||
})
|
||||
288
src/scaffold/workflow-routes.gen.ts
Normal file
288
src/scaffold/workflow-routes.gen.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
/**
|
||||
* This file was generated by @pikku/cli@0.12.23
|
||||
*/
|
||||
/**
|
||||
* Workflow HTTP catch-all routes
|
||||
* Do not edit manually - regenerate with 'npx pikku'
|
||||
*/
|
||||
import { pikkuSessionlessFunc, wireHTTPRoutes } from '@project/functions/.pikku/pikku-types.gen.js'
|
||||
import { MissingServiceError } from '@pikku/core/errors'
|
||||
import type { WorkflowRunStatus } from '@pikku/core/workflow'
|
||||
|
||||
function assertWorkflowService(workflowService: unknown): asserts workflowService {
|
||||
if (!workflowService) throw new MissingServiceError('workflowService is required')
|
||||
}
|
||||
|
||||
function assertWorkflowRunService(workflowRunService: unknown): asserts workflowRunService {
|
||||
if (!workflowRunService) throw new MissingServiceError('workflowRunService is required')
|
||||
}
|
||||
|
||||
export const workflowStarter = pikkuSessionlessFunc<
|
||||
{ workflowName: string; data?: unknown },
|
||||
{ runId: string }
|
||||
>({
|
||||
tags: ['pikku'],
|
||||
auth: false,
|
||||
// workflowService is destructured (even though we delegate via rpc) so the
|
||||
// analyzer assigns workflow-state capability to this unit — without it,
|
||||
// rpc.startWorkflow() runs against a container missing workflowService.
|
||||
func: async ({ workflowService }, { workflowName, data }, { rpc }) => {
|
||||
assertWorkflowService(workflowService)
|
||||
return await rpc.startWorkflow(workflowName as any, (data ?? {}) as any)
|
||||
},
|
||||
})
|
||||
|
||||
export const workflowRunner = pikkuSessionlessFunc<
|
||||
{ workflowName: string; data?: unknown },
|
||||
unknown
|
||||
>({
|
||||
tags: ['pikku'],
|
||||
auth: false,
|
||||
func: async ({ workflowService }, { workflowName, data }, { rpc }) => {
|
||||
assertWorkflowService(workflowService)
|
||||
return await workflowService.runToCompletion(workflowName, data ?? {}, rpc)
|
||||
},
|
||||
})
|
||||
|
||||
export const workflowStatusChecker = pikkuSessionlessFunc<
|
||||
{ workflowName: string; runId: string },
|
||||
WorkflowRunStatus
|
||||
>({
|
||||
tags: ['pikku'],
|
||||
auth: false,
|
||||
func: async ({ workflowService }, { runId }) => {
|
||||
assertWorkflowService(workflowService)
|
||||
const status = await workflowService.getRunStatus(runId)
|
||||
if (!status) throw new Error(`Run not found: ${runId}`)
|
||||
return status
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Minimal workflow status stream — sends step names and statuses only.
|
||||
* Use this for user-facing frontends where internal details should not be exposed.
|
||||
*/
|
||||
export const workflowStatusStream = pikkuSessionlessFunc<
|
||||
{ workflowName: string; runId: string },
|
||||
unknown
|
||||
>({
|
||||
tags: ['pikku'],
|
||||
auth: false,
|
||||
func: async ({ workflowRunService }, { runId }, { channel }) => {
|
||||
assertWorkflowRunService(workflowRunService)
|
||||
if (!channel) return
|
||||
|
||||
const terminalStatuses = new Set(['completed', 'failed', 'cancelled'])
|
||||
let lastHash = ''
|
||||
let initSent = false
|
||||
|
||||
const poll = async () => {
|
||||
const run = await workflowRunService.getRun(runId)
|
||||
if (!run) {
|
||||
channel.close()
|
||||
return false
|
||||
}
|
||||
|
||||
const steps = await workflowRunService.getRunSteps(runId)
|
||||
|
||||
if (!initSent && run.deterministic) {
|
||||
const statusByStep = new Map(
|
||||
steps.map((s: { stepName: string; status: string }) => [
|
||||
s.stepName,
|
||||
s.status,
|
||||
])
|
||||
)
|
||||
channel.send({
|
||||
type: 'init',
|
||||
deterministic: true,
|
||||
steps: (run.plannedSteps ?? []).map(
|
||||
(s: { stepName: string }) => ({
|
||||
stepName: s.stepName,
|
||||
status: statusByStep.get(s.stepName) ?? 'pending',
|
||||
})
|
||||
),
|
||||
})
|
||||
initSent = true
|
||||
}
|
||||
|
||||
const hash = JSON.stringify({
|
||||
s: run.status,
|
||||
steps: steps.map((s: { stepName: string; status: string }) => [s.stepName, s.status]),
|
||||
})
|
||||
|
||||
if (hash !== lastHash) {
|
||||
lastHash = hash
|
||||
channel.send({
|
||||
type: 'update',
|
||||
status: run.status,
|
||||
steps: steps.map((s: { stepName: string; status: string }) => ({
|
||||
stepName: s.stepName,
|
||||
status: s.status,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
if (terminalStatuses.has(run.status)) {
|
||||
channel.send({ type: 'done' })
|
||||
channel.close()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const shouldContinue = await poll()
|
||||
if (!shouldContinue) return
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
const interval = setInterval(async () => {
|
||||
const cont = await poll()
|
||||
if (!cont) {
|
||||
clearInterval(interval)
|
||||
resolve()
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Full workflow status stream — includes output, error, and child run IDs.
|
||||
* Use this for admin consoles and internal tooling.
|
||||
*/
|
||||
export const workflowStatusStreamFull = pikkuSessionlessFunc<
|
||||
{ workflowName: string; runId: string },
|
||||
unknown
|
||||
>({
|
||||
tags: ['pikku'],
|
||||
auth: false,
|
||||
func: async ({ workflowRunService }, { runId }, { channel }) => {
|
||||
assertWorkflowRunService(workflowRunService)
|
||||
if (!channel) return
|
||||
|
||||
const terminalStatuses = new Set(['completed', 'failed', 'cancelled'])
|
||||
let lastHash = ''
|
||||
let initSent = false
|
||||
|
||||
const poll = async () => {
|
||||
const run = await workflowRunService.getRun(runId)
|
||||
if (!run) {
|
||||
channel.close()
|
||||
return false
|
||||
}
|
||||
|
||||
const steps = await workflowRunService.getRunSteps(runId)
|
||||
|
||||
if (!initSent && run.deterministic) {
|
||||
const statusByStep = new Map(
|
||||
steps.map((s: { stepName: string; status: string }) => [
|
||||
s.stepName,
|
||||
s.status,
|
||||
])
|
||||
)
|
||||
channel.send({
|
||||
type: 'init',
|
||||
deterministic: true,
|
||||
steps: (run.plannedSteps ?? []).map(
|
||||
(s: { stepName: string }) => ({
|
||||
stepName: s.stepName,
|
||||
status: statusByStep.get(s.stepName) ?? 'pending',
|
||||
})
|
||||
),
|
||||
})
|
||||
initSent = true
|
||||
}
|
||||
|
||||
const hash = JSON.stringify({
|
||||
s: run.status,
|
||||
o: run.output,
|
||||
steps: steps.map((s: { stepName: string; status: string }) => [s.stepName, s.status]),
|
||||
})
|
||||
|
||||
if (hash !== lastHash) {
|
||||
lastHash = hash
|
||||
channel.send({
|
||||
type: 'update',
|
||||
status: run.status,
|
||||
output: run.output,
|
||||
error: run.error,
|
||||
steps: steps.map((s: { stepName: string; status: string; childRunId?: string }) => ({
|
||||
stepName: s.stepName,
|
||||
status: s.status,
|
||||
...(s.childRunId ? { childRunId: s.childRunId } : {}),
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
if (terminalStatuses.has(run.status)) {
|
||||
channel.send({ type: 'done' })
|
||||
channel.close()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const shouldContinue = await poll()
|
||||
if (!shouldContinue) return
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
const interval = setInterval(async () => {
|
||||
const cont = await poll()
|
||||
if (!cont) {
|
||||
clearInterval(interval)
|
||||
resolve()
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const graphStarter = pikkuSessionlessFunc<
|
||||
{ workflowName: string; nodeId: string; data?: unknown },
|
||||
{ runId: string }
|
||||
>({
|
||||
auth: false,
|
||||
// See workflowStarter — destructure workflowService so the analyzer
|
||||
// assigns workflow-state capability to this unit.
|
||||
func: async ({ workflowService }, { workflowName, nodeId, data }, { rpc }) => {
|
||||
assertWorkflowService(workflowService)
|
||||
return await rpc.startWorkflow(workflowName as any, (data ?? {}) as any, { startNode: nodeId })
|
||||
},
|
||||
})
|
||||
|
||||
wireHTTPRoutes({
|
||||
auth: false,
|
||||
routes: {
|
||||
workflowStart: {
|
||||
route: '/workflow/:workflowName/start',
|
||||
method: 'post',
|
||||
func: workflowStarter,
|
||||
},
|
||||
workflowRun: {
|
||||
route: '/workflow/:workflowName/run',
|
||||
method: 'post',
|
||||
func: workflowRunner,
|
||||
},
|
||||
workflowStatus: {
|
||||
route: '/workflow/:workflowName/status/:runId',
|
||||
method: 'get',
|
||||
func: workflowStatusChecker,
|
||||
},
|
||||
workflowStatusStream: {
|
||||
route: '/workflow/:workflowName/status/:runId/stream',
|
||||
method: 'get',
|
||||
sse: true,
|
||||
func: workflowStatusStream,
|
||||
},
|
||||
workflowStatusStreamFull: {
|
||||
route: '/workflow/:workflowName/status/:runId/stream/full',
|
||||
method: 'get',
|
||||
sse: true,
|
||||
func: workflowStatusStreamFull,
|
||||
},
|
||||
graphStart: {
|
||||
route: '/workflow/:workflowName/graph/:nodeId',
|
||||
method: 'post',
|
||||
func: graphStarter,
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user