@hanfani/core 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 +21 -0
- package/README.md +47 -0
- package/dist/conformance.d.ts +25 -0
- package/dist/conformance.d.ts.map +1 -0
- package/dist/conformance.js +90 -0
- package/dist/conformance.js.map +1 -0
- package/dist/defineAgent.d.ts +76 -0
- package/dist/defineAgent.d.ts.map +1 -0
- package/dist/defineAgent.js +59 -0
- package/dist/defineAgent.js.map +1 -0
- package/dist/definePrompt.d.ts +15 -0
- package/dist/definePrompt.d.ts.map +1 -0
- package/dist/definePrompt.js +23 -0
- package/dist/definePrompt.js.map +1 -0
- package/dist/defineWorkflow.d.ts +10 -0
- package/dist/defineWorkflow.d.ts.map +1 -0
- package/dist/defineWorkflow.js +38 -0
- package/dist/defineWorkflow.js.map +1 -0
- package/dist/delivery.d.ts +8 -0
- package/dist/delivery.d.ts.map +1 -0
- package/dist/delivery.js +21 -0
- package/dist/delivery.js.map +1 -0
- package/dist/fold.d.ts +8 -0
- package/dist/fold.d.ts.map +1 -0
- package/dist/fold.js +98 -0
- package/dist/fold.js.map +1 -0
- package/dist/gate.d.ts +25 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +22 -0
- package/dist/gate.js.map +1 -0
- package/dist/handoff.d.ts +77 -0
- package/dist/handoff.d.ts.map +1 -0
- package/dist/handoff.js +52 -0
- package/dist/handoff.js.map +1 -0
- package/dist/handoffNote.d.ts +11 -0
- package/dist/handoffNote.d.ts.map +1 -0
- package/dist/handoffNote.js +6 -0
- package/dist/handoffNote.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/integration.d.ts +9 -0
- package/dist/integration.d.ts.map +1 -0
- package/dist/integration.js +13 -0
- package/dist/integration.js.map +1 -0
- package/dist/integrationAuth.d.ts +8 -0
- package/dist/integrationAuth.d.ts.map +1 -0
- package/dist/integrationAuth.js +5 -0
- package/dist/integrationAuth.js.map +1 -0
- package/dist/lifecycle.d.ts +16 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +57 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/lifecycleNote.d.ts +13 -0
- package/dist/lifecycleNote.d.ts.map +1 -0
- package/dist/lifecycleNote.js +17 -0
- package/dist/lifecycleNote.js.map +1 -0
- package/dist/messages.d.ts +21 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +78 -0
- package/dist/messages.js.map +1 -0
- package/dist/prompt.d.ts +3 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +6 -0
- package/dist/prompt.js.map +1 -0
- package/dist/providers.d.ts +4 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/providers.js +12 -0
- package/dist/providers.js.map +1 -0
- package/dist/types.d.ts +158 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +43 -0
- package/src/conformance.ts +118 -0
- package/src/defineAgent.ts +63 -0
- package/src/definePrompt.ts +33 -0
- package/src/defineWorkflow.ts +52 -0
- package/src/delivery.ts +28 -0
- package/src/fold.ts +129 -0
- package/src/gate.ts +26 -0
- package/src/handoff.ts +58 -0
- package/src/handoffNote.ts +15 -0
- package/src/index.ts +101 -0
- package/src/integration.ts +14 -0
- package/src/integrationAuth.ts +8 -0
- package/src/lifecycle.ts +61 -0
- package/src/lifecycleNote.ts +27 -0
- package/src/messages.ts +99 -0
- package/src/prompt.ts +8 -0
- package/src/providers.ts +12 -0
- package/src/types.ts +151 -0
package/src/handoff.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { Message, RunAgentInput } from '@ag-ui/client'
|
|
3
|
+
|
|
4
|
+
export const HandoffPayloadSchema = z.object({
|
|
5
|
+
threadId: z.string(),
|
|
6
|
+
from: z.string(),
|
|
7
|
+
subject: z.string(),
|
|
8
|
+
summary: z.string(),
|
|
9
|
+
category: z.string(),
|
|
10
|
+
priority: z.string(),
|
|
11
|
+
})
|
|
12
|
+
export type HandoffPayload = z.infer<typeof HandoffPayloadSchema>
|
|
13
|
+
|
|
14
|
+
export const TicketHandoffPayloadSchema = z.object({
|
|
15
|
+
repo: z.string(),
|
|
16
|
+
number: z.number(),
|
|
17
|
+
title: z.string(),
|
|
18
|
+
status: z.string(),
|
|
19
|
+
priority: z.string(),
|
|
20
|
+
body: z.string(),
|
|
21
|
+
lastComment: z
|
|
22
|
+
.object({
|
|
23
|
+
author: z.string(),
|
|
24
|
+
body: z.string(),
|
|
25
|
+
})
|
|
26
|
+
.nullable(),
|
|
27
|
+
recommendation: z.string(),
|
|
28
|
+
url: z.string(),
|
|
29
|
+
})
|
|
30
|
+
export type TicketHandoffPayload = z.infer<typeof TicketHandoffPayloadSchema>
|
|
31
|
+
|
|
32
|
+
const HANDOFF_PREFIX = '[handoff]'
|
|
33
|
+
|
|
34
|
+
/** Encode a payload as a user message that downstream agents can decode. */
|
|
35
|
+
export function encodeHandoff(payload: unknown): Message {
|
|
36
|
+
return {
|
|
37
|
+
id: crypto.randomUUID(),
|
|
38
|
+
role: 'user',
|
|
39
|
+
content: `${HANDOFF_PREFIX} ${JSON.stringify(payload)}`,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Decode the most recent handoff payload in the run input, validated by `schema`. */
|
|
44
|
+
export function decodeHandoff<T>(input: RunAgentInput, schema: z.ZodType<T>): T | null {
|
|
45
|
+
const messages = input?.messages ?? []
|
|
46
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
47
|
+
const m = messages[i]
|
|
48
|
+
if (m && m.role === 'user' && typeof m.content === 'string' && m.content.startsWith(HANDOFF_PREFIX)) {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = schema.safeParse(JSON.parse(m.content.slice(HANDOFF_PREFIX.length).trim()))
|
|
51
|
+
return parsed.success ? parsed.data : null
|
|
52
|
+
} catch {
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { EventType } from '@ag-ui/client'
|
|
2
|
+
import type { BaseEvent } from '@ag-ui/client'
|
|
3
|
+
|
|
4
|
+
export interface HandoffNoteValue {
|
|
5
|
+
kind: 'handoff'
|
|
6
|
+
targetAgentId: string
|
|
7
|
+
childWorkItemId: string
|
|
8
|
+
deduped: boolean
|
|
9
|
+
at: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Emit a handoff note event. */
|
|
13
|
+
export function handoffNote(value: HandoffNoteValue): BaseEvent {
|
|
14
|
+
return { type: EventType.CUSTOM, name: 'handoff', value } as unknown as BaseEvent
|
|
15
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// @hanfani/core — headless engine and types for the Hanfani agent framework.
|
|
2
|
+
//
|
|
3
|
+
// The one invariant this package encodes: an agent proposes, a human approves,
|
|
4
|
+
// the server acts. `defineAgent` refuses any effect that isn't gated behind an
|
|
5
|
+
// approval; the conformance suite proves providers honour that at runtime.
|
|
6
|
+
|
|
7
|
+
// Re-export the two @ag-ui/client types that appear in the public API.
|
|
8
|
+
export type { Message, ToolCall } from '@ag-ui/client'
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
AgentDefinitionSchema,
|
|
12
|
+
defineAgent,
|
|
13
|
+
type AgentDefinition,
|
|
14
|
+
type AgentDefinitionInput,
|
|
15
|
+
} from './defineAgent.js'
|
|
16
|
+
|
|
17
|
+
export { defineWorkflow, instanceId } from './defineWorkflow.js'
|
|
18
|
+
|
|
19
|
+
export { definePrompt, type PromptSpec } from './definePrompt.js'
|
|
20
|
+
|
|
21
|
+
export { defineProviders } from './providers.js'
|
|
22
|
+
|
|
23
|
+
export { composeInstructions } from './prompt.js'
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
approvalResolved,
|
|
27
|
+
hasPendingApproval,
|
|
28
|
+
isAssistant,
|
|
29
|
+
isToolMessage,
|
|
30
|
+
lastApprovalArgs,
|
|
31
|
+
pairToolResults,
|
|
32
|
+
resolvedApprovalCount,
|
|
33
|
+
toolCallsOf,
|
|
34
|
+
type AssistantMessage,
|
|
35
|
+
type ToolMessage,
|
|
36
|
+
} from './messages.js'
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
decodeHandoff,
|
|
40
|
+
encodeHandoff,
|
|
41
|
+
HandoffPayloadSchema,
|
|
42
|
+
TicketHandoffPayloadSchema,
|
|
43
|
+
type HandoffPayload,
|
|
44
|
+
type TicketHandoffPayload,
|
|
45
|
+
} from './handoff.js'
|
|
46
|
+
|
|
47
|
+
export { handoffNote, type HandoffNoteValue } from './handoffNote.js'
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
GATE_OPENED,
|
|
51
|
+
gateOpened,
|
|
52
|
+
GateOpenedValueSchema,
|
|
53
|
+
readGateOpened,
|
|
54
|
+
type GateOpenedValue,
|
|
55
|
+
} from './gate.js'
|
|
56
|
+
|
|
57
|
+
export { LIFECYCLE_NOTE_TEXT, lifecycleNote, type LifecycleNoteValue } from './lifecycleNote.js'
|
|
58
|
+
|
|
59
|
+
export { hasLiveDescendant, lifecycle } from './lifecycle.js'
|
|
60
|
+
|
|
61
|
+
export { foldEventsToMessages } from './fold.js'
|
|
62
|
+
|
|
63
|
+
export { resolveDelivery } from './delivery.js'
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
providerConformanceChecks,
|
|
67
|
+
type ConformanceCheck,
|
|
68
|
+
type ConformanceScenario,
|
|
69
|
+
} from './conformance.js'
|
|
70
|
+
|
|
71
|
+
export { aggregateHealth, isOk } from './integration.js'
|
|
72
|
+
|
|
73
|
+
export { isOAuth2 } from './integrationAuth.js'
|
|
74
|
+
|
|
75
|
+
export type {
|
|
76
|
+
AgentRole,
|
|
77
|
+
AuthSpec,
|
|
78
|
+
BatchActionResult,
|
|
79
|
+
CredentialResolver,
|
|
80
|
+
DeliveryResult,
|
|
81
|
+
Destination,
|
|
82
|
+
EffectFn,
|
|
83
|
+
GateResolution,
|
|
84
|
+
HealthCheck,
|
|
85
|
+
Lifecycle,
|
|
86
|
+
Outcome,
|
|
87
|
+
Phase,
|
|
88
|
+
PromptStrategy,
|
|
89
|
+
Provider,
|
|
90
|
+
ProviderConfig,
|
|
91
|
+
ProviderFactory,
|
|
92
|
+
ProviderRegistry,
|
|
93
|
+
ReadResult,
|
|
94
|
+
ResolvedCredential,
|
|
95
|
+
ResumeHandle,
|
|
96
|
+
ResumeOutcome,
|
|
97
|
+
WorkflowAgent,
|
|
98
|
+
WorkflowConnection,
|
|
99
|
+
WorkflowDescriptor,
|
|
100
|
+
WorkflowInput,
|
|
101
|
+
} from './types.js'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { HealthCheck } from './types.js'
|
|
2
|
+
|
|
3
|
+
/** Narrow a HealthCheck to its ok variant. */
|
|
4
|
+
export function isOk(h: HealthCheck): h is { ok: true; detail?: string } {
|
|
5
|
+
return h.ok
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Return the first failing check, or an ok result if all pass. */
|
|
9
|
+
export function aggregateHealth(checks: HealthCheck[]): HealthCheck {
|
|
10
|
+
for (const check of checks) {
|
|
11
|
+
if (!check.ok) return check
|
|
12
|
+
}
|
|
13
|
+
return { ok: true }
|
|
14
|
+
}
|
package/src/lifecycle.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Lifecycle, Outcome, Phase } from './types.js'
|
|
2
|
+
|
|
3
|
+
const LIVE_PHASES = new Set<Phase>(['queued', 'active', 'awaiting_human'])
|
|
4
|
+
const SILENT_OUTCOMES = new Set<Outcome>(['superseded', 'reset', 'dismissed'])
|
|
5
|
+
const FAILED_OUTCOMES = new Set<Outcome>(['stopped', 'rejected', 'error'])
|
|
6
|
+
const COVERING_OUTCOMES = new Set<Outcome>(['done', 'stopped', 'rejected'])
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Derive display lifecycle from a work item's phase/outcome plus whether it has
|
|
10
|
+
* a render card and whether it has a live descendant.
|
|
11
|
+
*/
|
|
12
|
+
export function lifecycle(
|
|
13
|
+
phase: Phase,
|
|
14
|
+
outcome: Outcome,
|
|
15
|
+
hasCard: boolean,
|
|
16
|
+
hasLiveDescendant: boolean,
|
|
17
|
+
): Lifecycle {
|
|
18
|
+
const isLive = LIVE_PHASES.has(phase)
|
|
19
|
+
let isVisible: boolean
|
|
20
|
+
if (phase === 'queued') isVisible = false
|
|
21
|
+
else if (isLive) isVisible = true
|
|
22
|
+
else if (SILENT_OUTCOMES.has(outcome)) isVisible = false
|
|
23
|
+
else isVisible = hasCard || FAILED_OUTCOMES.has(outcome) || hasLiveDescendant
|
|
24
|
+
const covers = isLive || COVERING_OUTCOMES.has(outcome)
|
|
25
|
+
return { phase, outcome, isLive, isVisible, covers }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Given flat parent/child rows, return the set of ids that have at least one
|
|
30
|
+
* live descendant (a queued/active/awaiting_human node somewhere below them).
|
|
31
|
+
*/
|
|
32
|
+
export function hasLiveDescendant<T extends { id: string; parentId: string | null; phase: Phase }>(
|
|
33
|
+
rows: readonly T[],
|
|
34
|
+
): Set<string> {
|
|
35
|
+
const childrenByParent = new Map<string, T[]>()
|
|
36
|
+
for (const row of rows) {
|
|
37
|
+
if (!row.parentId) continue
|
|
38
|
+
const siblings = childrenByParent.get(row.parentId) ?? []
|
|
39
|
+
siblings.push(row)
|
|
40
|
+
childrenByParent.set(row.parentId, siblings)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const memo = new Map<string, boolean>()
|
|
44
|
+
const visit = (id: string): boolean => {
|
|
45
|
+
const cached = memo.get(id)
|
|
46
|
+
if (cached !== undefined) return cached
|
|
47
|
+
memo.set(id, false) // guard against cycles
|
|
48
|
+
let live = false
|
|
49
|
+
for (const child of childrenByParent.get(id) ?? []) {
|
|
50
|
+
if (LIVE_PHASES.has(child.phase) || visit(child.id)) live = true
|
|
51
|
+
}
|
|
52
|
+
memo.set(id, live)
|
|
53
|
+
return live
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const result = new Set<string>()
|
|
57
|
+
for (const row of rows) {
|
|
58
|
+
if (visit(row.id)) result.add(row.id)
|
|
59
|
+
}
|
|
60
|
+
return result
|
|
61
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { EventType } from '@ag-ui/client'
|
|
2
|
+
import type { BaseEvent } from '@ag-ui/client'
|
|
3
|
+
import type { Outcome } from './types.js'
|
|
4
|
+
|
|
5
|
+
export interface LifecycleNoteValue {
|
|
6
|
+
kind: 'lifecycle'
|
|
7
|
+
outcome: Outcome
|
|
8
|
+
actor: string | null
|
|
9
|
+
at: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Emit a lifecycle note event. */
|
|
13
|
+
export function lifecycleNote(value: LifecycleNoteValue): BaseEvent {
|
|
14
|
+
return { type: EventType.CUSTOM, name: 'lifecycle', value } as unknown as BaseEvent
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Human-readable label shown for each terminal outcome. */
|
|
18
|
+
export const LIFECYCLE_NOTE_TEXT: Record<Outcome, string> = {
|
|
19
|
+
running: '',
|
|
20
|
+
done: 'Done',
|
|
21
|
+
stopped: 'Stopped — cancelled',
|
|
22
|
+
rejected: 'Rejected',
|
|
23
|
+
error: 'Error',
|
|
24
|
+
superseded: 'Superseded by a re-run',
|
|
25
|
+
reset: 'Cleared from board',
|
|
26
|
+
dismissed: 'Error acknowledged — dismissed',
|
|
27
|
+
}
|
package/src/messages.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Message, ToolCall } from '@ag-ui/client'
|
|
2
|
+
|
|
3
|
+
export type AssistantMessage = Extract<Message, { role: 'assistant' }>
|
|
4
|
+
export type ToolMessage = Extract<Message, { role: 'tool' }>
|
|
5
|
+
|
|
6
|
+
export function isAssistant(m: Message): m is AssistantMessage {
|
|
7
|
+
return m.role === 'assistant'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isToolMessage(m: Message): m is ToolMessage {
|
|
11
|
+
return m.role === 'tool'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function toolCallsOf(m: Message): ToolCall[] {
|
|
15
|
+
return isAssistant(m) && Array.isArray(m.toolCalls) ? m.toolCalls : []
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** True when an approval tool has been called but has no tool result yet. */
|
|
19
|
+
export function hasPendingApproval(
|
|
20
|
+
messages: readonly Message[],
|
|
21
|
+
approvalNames: readonly string[],
|
|
22
|
+
): boolean {
|
|
23
|
+
const resolvedIds = new Set<string>()
|
|
24
|
+
for (const m of messages) {
|
|
25
|
+
if (isToolMessage(m) && typeof m.toolCallId === 'string') resolvedIds.add(m.toolCallId)
|
|
26
|
+
}
|
|
27
|
+
for (const m of messages) {
|
|
28
|
+
for (const call of toolCallsOf(m)) {
|
|
29
|
+
if (
|
|
30
|
+
approvalNames.includes(call.function.name) &&
|
|
31
|
+
typeof call.id === 'string' &&
|
|
32
|
+
!resolvedIds.has(call.id)
|
|
33
|
+
) {
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Map each tool result message by the toolCallId it answers. */
|
|
42
|
+
export function pairToolResults(messages: readonly Message[]): Map<string, ToolMessage> {
|
|
43
|
+
const byCallId = new Map<string, ToolMessage>()
|
|
44
|
+
for (const m of messages) {
|
|
45
|
+
if (isToolMessage(m) && typeof m.toolCallId === 'string') byCallId.set(m.toolCallId, m)
|
|
46
|
+
}
|
|
47
|
+
return byCallId
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Parsed arguments of the most recent approval tool call, or null. */
|
|
51
|
+
export function lastApprovalArgs(
|
|
52
|
+
messages: readonly Message[],
|
|
53
|
+
approvalNames: readonly string[],
|
|
54
|
+
): Record<string, unknown> | null {
|
|
55
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
56
|
+
const m = messages[i]
|
|
57
|
+
if (!m) continue
|
|
58
|
+
for (const call of toolCallsOf(m)) {
|
|
59
|
+
if (approvalNames.includes(call.function.name)) {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(call.function.arguments)
|
|
62
|
+
} catch {
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** How many approval tool calls have a matching tool result. */
|
|
72
|
+
export function resolvedApprovalCount(
|
|
73
|
+
messages: readonly Message[],
|
|
74
|
+
approvalNames: readonly string[],
|
|
75
|
+
): number {
|
|
76
|
+
const approvalCallIds = new Set<string>()
|
|
77
|
+
for (const m of messages) {
|
|
78
|
+
for (const call of toolCallsOf(m)) {
|
|
79
|
+
if (approvalNames.includes(call.function.name) && typeof call.id === 'string') {
|
|
80
|
+
approvalCallIds.add(call.id)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const resolved = new Set<string>()
|
|
85
|
+
for (const m of messages) {
|
|
86
|
+
if (isToolMessage(m) && typeof m.toolCallId === 'string' && approvalCallIds.has(m.toolCallId)) {
|
|
87
|
+
resolved.add(m.toolCallId)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return resolved.size
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** True when at least one approval call has resolved. */
|
|
94
|
+
export function approvalResolved(
|
|
95
|
+
messages: readonly Message[],
|
|
96
|
+
approvalNames: readonly string[],
|
|
97
|
+
): boolean {
|
|
98
|
+
return resolvedApprovalCount(messages, approvalNames) > 0
|
|
99
|
+
}
|
package/src/prompt.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Prepend the workflow-level prompt (if any) to an agent's own instructions. */
|
|
2
|
+
export function composeInstructions(
|
|
3
|
+
workflowPrompt: string | undefined,
|
|
4
|
+
agentInstructions: string,
|
|
5
|
+
): string {
|
|
6
|
+
const prefix = workflowPrompt?.trim()
|
|
7
|
+
return prefix ? `${prefix}\n\n${agentInstructions}` : agentInstructions
|
|
8
|
+
}
|
package/src/providers.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ProviderFactory, ProviderRegistry } from './types.js'
|
|
2
|
+
|
|
3
|
+
/** Build a registry that resolves provider factories by name. */
|
|
4
|
+
export function defineProviders(map: Record<string, ProviderFactory>): ProviderRegistry {
|
|
5
|
+
return {
|
|
6
|
+
resolve(name: string): ProviderFactory {
|
|
7
|
+
const factory = map[name]
|
|
8
|
+
if (!factory) throw new Error(`Unknown provider: ${name}`)
|
|
9
|
+
return factory
|
|
10
|
+
},
|
|
11
|
+
}
|
|
12
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { BaseEvent, Message, RunAgentInput, ToolCall } from '@ag-ui/client'
|
|
2
|
+
import type { z } from 'zod'
|
|
3
|
+
import type { AgentDefinition } from './defineAgent.js'
|
|
4
|
+
|
|
5
|
+
// Re-export the two @ag-ui/client types that appear in this package's public API,
|
|
6
|
+
// so consumers can import them from @hanfani/core directly.
|
|
7
|
+
export type { Message, ToolCall }
|
|
8
|
+
|
|
9
|
+
/** Lifecycle phase of a work item. */
|
|
10
|
+
export type Phase = 'queued' | 'active' | 'awaiting_human' | 'terminal'
|
|
11
|
+
|
|
12
|
+
/** Terminal (and running) outcome of a work item. */
|
|
13
|
+
export type Outcome =
|
|
14
|
+
| 'running'
|
|
15
|
+
| 'done'
|
|
16
|
+
| 'stopped'
|
|
17
|
+
| 'rejected'
|
|
18
|
+
| 'error'
|
|
19
|
+
| 'superseded'
|
|
20
|
+
| 'reset'
|
|
21
|
+
| 'dismissed'
|
|
22
|
+
|
|
23
|
+
/** Whether an agent accepts external input or only runs as a downstream worker. */
|
|
24
|
+
export type AgentRole = 'input' | 'worker'
|
|
25
|
+
|
|
26
|
+
/** Result of a health probe. */
|
|
27
|
+
export type HealthCheck =
|
|
28
|
+
| { ok: true; detail?: string }
|
|
29
|
+
| { ok: false; error: string; hint: string }
|
|
30
|
+
|
|
31
|
+
/** How an integration authenticates. */
|
|
32
|
+
export type AuthSpec =
|
|
33
|
+
| { kind: 'none' }
|
|
34
|
+
| { kind: 'apiKey' }
|
|
35
|
+
| { kind: 'oauth2'; provider: string; scopes: string[] }
|
|
36
|
+
| { kind: string; [key: string]: unknown }
|
|
37
|
+
|
|
38
|
+
/** A credential resolved for a given integration + connection. */
|
|
39
|
+
export type ResolvedCredential =
|
|
40
|
+
| { kind: 'apiKey'; apiKey: string }
|
|
41
|
+
| { kind: 'oauth2'; accessToken: string; refreshToken?: string; expiresAt?: number; raw?: unknown }
|
|
42
|
+
| { kind: string; [key: string]: unknown }
|
|
43
|
+
|
|
44
|
+
export type CredentialResolver = (ctx: {
|
|
45
|
+
integration: string
|
|
46
|
+
connectionId: string
|
|
47
|
+
auth: AuthSpec
|
|
48
|
+
}) => Promise<ResolvedCredential | null>
|
|
49
|
+
|
|
50
|
+
/** What a prompt strategy decides to do when a gate resolves. */
|
|
51
|
+
export type ResumeOutcome =
|
|
52
|
+
| { kind: 'prompt'; text: string }
|
|
53
|
+
| { kind: 'message'; text: string }
|
|
54
|
+
| null
|
|
55
|
+
|
|
56
|
+
export interface ResumeHandle {
|
|
57
|
+
runId: string
|
|
58
|
+
input: RunAgentInput
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface GateResolution {
|
|
62
|
+
gateId: string
|
|
63
|
+
decision: 'approved' | 'rejected'
|
|
64
|
+
form?: Record<string, unknown>
|
|
65
|
+
comment?: string
|
|
66
|
+
executedResult?: Record<string, unknown>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface PromptStrategy {
|
|
70
|
+
buildFirst(input: RunAgentInput): string
|
|
71
|
+
buildResume?(args: Record<string, unknown>, executedResult?: Record<string, unknown>): ResumeOutcome
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface Provider {
|
|
75
|
+
run(input: RunAgentInput): AsyncIterable<BaseEvent>
|
|
76
|
+
resume?(handle: ResumeHandle, resolution: GateResolution): AsyncIterable<BaseEvent>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ProviderConfig {
|
|
80
|
+
approvalNames: readonly string[]
|
|
81
|
+
surfaceTools: readonly string[]
|
|
82
|
+
allowedTools: readonly string[]
|
|
83
|
+
prompts: PromptStrategy
|
|
84
|
+
instructions: string
|
|
85
|
+
agentId: string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type ProviderFactory = (config: ProviderConfig) => Provider
|
|
89
|
+
|
|
90
|
+
export interface ProviderRegistry {
|
|
91
|
+
resolve(name: string): ProviderFactory
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface Lifecycle {
|
|
95
|
+
phase: Phase
|
|
96
|
+
outcome: Outcome
|
|
97
|
+
isLive: boolean
|
|
98
|
+
isVisible: boolean
|
|
99
|
+
covers: boolean
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Where a produced payload should be delivered. */
|
|
103
|
+
export type Destination =
|
|
104
|
+
| { kind: 'agent'; agentId: string }
|
|
105
|
+
| { kind: 'contract'; workflow: string; input: string }
|
|
106
|
+
|
|
107
|
+
export type DeliveryResult =
|
|
108
|
+
| { ok: true; instanceId: string; targetWorkflow?: string }
|
|
109
|
+
| { ok: false; error: string }
|
|
110
|
+
|
|
111
|
+
/** The consequential side effect bound to an approved tool. Runs on the server only. */
|
|
112
|
+
export type EffectFn = (
|
|
113
|
+
form: Record<string, unknown>,
|
|
114
|
+
ctx: { workItemId: string; gateId: string },
|
|
115
|
+
) => Promise<Record<string, unknown>>
|
|
116
|
+
|
|
117
|
+
export type WorkflowAgent = {
|
|
118
|
+
agent: AgentDefinition
|
|
119
|
+
role: AgentRole
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type WorkflowConnection = {
|
|
123
|
+
integration: string
|
|
124
|
+
connection?: string
|
|
125
|
+
provider: string
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export type WorkflowInput = {
|
|
129
|
+
name: string
|
|
130
|
+
schema: z.ZodTypeAny
|
|
131
|
+
agentId: string
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export type WorkflowDescriptor = {
|
|
135
|
+
id: string
|
|
136
|
+
label: string
|
|
137
|
+
iconName: string
|
|
138
|
+
agents: WorkflowAgent[]
|
|
139
|
+
entryAgentId: string
|
|
140
|
+
inputs: WorkflowInput[]
|
|
141
|
+
prompt?: string
|
|
142
|
+
connections?: WorkflowConnection[]
|
|
143
|
+
rerun?: 'refresh' | 'history'
|
|
144
|
+
resetOnStart?: boolean
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export type ReadResult<T> = T | { error: string }
|
|
148
|
+
|
|
149
|
+
export type BatchActionResult =
|
|
150
|
+
| { done: string[]; failed: { messageId: string; error: string }[] }
|
|
151
|
+
| { error: string }
|