Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/core/action.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ Each manager is a key-value store backed by a `Map`. See [BaseManager](./base-ma
description: 'Stores registered runtime function definitions keyed by runtimeName.',
type: 'RuntimeFunctionManager',
},
subFlowFunctions: {
description: 'Stores sub flow functions returned by a runtime function at execution time, keyed by the identifier reported to Aquila. Entries expire 30 minutes after they were registered.',
type: 'SubFlowFunctionManager',
},
dataTypes: {
description: 'Stores registered data type definitions keyed by identifier.',
type: 'DataTypeManager',
Expand Down Expand Up @@ -174,6 +178,19 @@ entry is also created automatically.
action.registerRuntimeFunction(FibonacciRuntimeFunction);
```

### `registerSubFlowFunction`

```ts
action.registerSubFlowFunction(definition: SubFlowFunctionDefinition): string
```

Registers a sub flow function returned from a `run()` block and returns the identifier
Aquila invokes it by. Called automatically while a result is sent, so a runtime
function only has to return the definition — see
[Returning a sub flow function](./runtime-function.mdx#returning-a-sub-flow-function).
The registration expires after 30 minutes
([`SubFlowFunctionManager`](./sub-flow-function-manager.mdx#lifetime)).

### `registerDataTypeClass`

```ts
Expand Down
1 change: 1 addition & 0 deletions docs/core/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"base-manager",
"function-manager",
"runtime-function-manager",
"sub-flow-function-manager",
"event-manager",
"runtime-event-manager",
"data-type-manager",
Expand Down
16 changes: 13 additions & 3 deletions docs/core/runtime-function-manager.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,23 @@ The interface implemented by all RuntimeFunction classes.

```ts
interface RuntimeFunctionRunnable {
run(...args: (PlainValue | undefined)[]): Promise<PlainValue> | PlainValue;
run(...args: (PlainValue | undefined)[]): Promise<RuntimeFunctionReturn> | RuntimeFunctionReturn;
}
```

The first argument to `run()` is always a [`FunctionContext`](#functioncontext) object. Subsequent arguments
correspond to the declared parameters in order.

### `RuntimeFunctionReturn`

What a `run()` may produce: raw data, or a sub flow function that Hercules registers
and reports to Aquila. See
[Returning a sub flow function](./runtime-function.mdx#returning-a-sub-flow-function).

```ts
type RuntimeFunctionReturn = PlainValue | SubFlowFunctionDefinition;
```

### `RuntimeFunctionProps`

The descriptor stored for each registered runtime function.
Expand All @@ -47,7 +57,7 @@ interface RuntimeFunctionProps {
alias?: Translation[];
displayIcon?: string;
design?: string;
handler: (...args: (PlainValue | undefined)[]) => Promise<PlainValue> | PlainValue;
handler: (...args: (PlainValue | undefined)[]) => Promise<RuntimeFunctionReturn> | RuntimeFunctionReturn;
}
```

Expand Down Expand Up @@ -107,7 +117,7 @@ interface RuntimeFunctionProps {
},
handler: {
description: 'Populated automatically from the class run() method during registration.',
type: '(...args: (PlainValue | undefined)[]) => Promise<PlainValue> | PlainValue',
type: '(...args: (PlainValue | undefined)[]) => Promise<RuntimeFunctionReturn> | RuntimeFunctionReturn',
required: true,
},
}}
Expand Down
52 changes: 52 additions & 0 deletions docs/core/runtime-function.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,58 @@ export class FibonacciRuntimeFunction {
}
```

## Returning a sub flow function

A `run()` block may return a **sub flow function** instead of raw data. Hercules
registers its `run` internally and reports the function to Aquila as a sub flow
function result, so a later node of the flow can invoke it like any other sub flow.

Because nothing in the module definition describes a function that only comes into
existence at runtime, its name, description and I/O schema have to be declared with it.
The schemas are written as [Zod](https://zod.dev) schemas — the same way
[`@Schema`](./decorator-schema.mdx) describes a data type — and are converted to JSON
schemas while the result is sent:

```ts
import { z } from 'zod';
import { SubFlowFunctionDefinition } from '@code0-tech/hercules';

@Identifier('greeter_runtime')
@Signature('(greeting: STRING): TRANSFORM<STRING, STRING>')
export class GreeterRuntimeFunction {
run(context: FunctionContext, greeting: string): SubFlowFunctionDefinition {
return {
name: 'greet',
description: `Greets a name with "${greeting}"`,
inputSchema: z.object({ name: z.string() }),
outputSchema: z.string(),
run: (name) => `${greeting} ${String(name)}`,
};
}
}
```

The input schema is an object keyed by parameter name, the output schema describes the
value the function returns. Each is generated from its own side of the zod schema —
the input schema from the schema's input side, the output schema from its output side
— so a schema with a transform or a default describes each direction correctly.

The [`@Signature`](./decorator-signature.mdx) return type is the function data type
the returned function matches — not the raw data type it eventually produces:

| Returned function | Signature return type |
|---|---|
| `(value) => result` | `TRANSFORM<T, R>` |
| `(value) => void` | `CONSUMER<T>` |
| `(value) => boolean` | `PREDICATE<T>` |
| `(a, b) => number` | `COMPARATOR<T>` |
| `() => void` | `RUNNABLE` |

Each returned function is registered under its own identifier, so returning one per
execution never overwrites a function a running flow still holds. A registration has a
maximum lifetime of **30 minutes** and is then dropped from memory — see
[`action.subFlowFunctions`](./sub-flow-function-manager.mdx).

## Error Handling

Throw a `RuntimeError` to signal a recoverable error back to the platform:
Expand Down
99 changes: 99 additions & 0 deletions docs/core/sub-flow-function-manager.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
title: SubFlowFunctionManager
description: Stores sub flow functions returned at execution time
---

import { TypeTable } from 'fumadocs-ui/components/type-table';

`SubFlowFunctionManager` stores the sub flow functions a runtime function returned from
its `run()` block (see
[Returning a sub flow function](./runtime-function.mdx#returning-a-sub-flow-function)).
It extends [`BaseManager`](./base-manager.mdx) with key type `string` (the identifier
reported to Aquila) and value type `SubFlowFunctionDefinition`.

Unlike the other managers it is not filled by registering classes up front: entries
appear while the action runs, each time a `run()` returns a function.

```ts
action.subFlowFunctions.size;
action.subFlowFunctions.delete(identifier);
```

## Lifetime

Aquila gives no signal that a flow is done with a sub flow function, so each entry has
a maximum lifetime of `SUB_FLOW_FUNCTION_TTL_MS` — **30 minutes** — counted from when
it was registered and never extended by an invocation. Once it passes, the function is
dropped from memory and can no longer be invoked, so the registry cannot grow without
bound while the action runs.

Expired entries are cleared by a sweep scheduled for the next expiry, so memory is
freed without waiting for another lookup, and additionally on every read and write, so
an expired function is never invoked even if the sweep could not run. Delete an entry
to drop it earlier:

```ts
action.subFlowFunctions.delete(identifier);
```

An execution request naming a function that has expired fails the node with a
`SUB_FLOW_FUNCTION_EXPIRED` error rather than going unanswered, so the flow reports a
clear failure instead of waiting for a result that cannot come. Identifiers this
action never minted are still only logged — they address a function the action does
not know at all.

## Interfaces

### `SubFlowFunctionDefinition`

Returned from a `run()` block in place of raw data.

```ts
interface SubFlowFunctionDefinition<R extends PlainValue = PlainValue> {
name: string;
description: string;
inputSchema: SubFlowFunctionSchema;
outputSchema: SubFlowFunctionSchema;
run(...args: (PlainValue | undefined)[]): Promise<R> | R;
}
```

<TypeTable
type={{
name: {
description: 'Name of the function as the platform shows it.',
type: 'string',
required: true,
},
description: {
description: 'Short description of what the function does.',
type: 'string',
required: true,
},
inputSchema: {
description: 'Zod schema describing what the function accepts. Converted to a JSON schema when the result is sent.',
type: 'SubFlowFunctionSchema',
required: true,
},
outputSchema: {
description: 'Zod schema describing what the function returns. Converted to a JSON schema when the result is sent.',
type: 'SubFlowFunctionSchema',
required: true,
},
run: {
description: 'The implementation. Called with the parameters Aquila sends when the flow invokes the function, preceded by a FunctionContext when the signature declares one.',
type: '(...args: (PlainValue | undefined)[]) => Promise<PlainValue> | PlainValue',
required: true,
},
}}
/>

### `SubFlowFunctionSchema`

Normally a Zod schema, converted to a JSON schema when the result is sent. A plain
JSON schema object or a ready-made protobuf `Struct` — e.g. the `inputSchema` of a
[`SubFlow`](./runtime-function.mdx) parameter — is passed through unchanged.

```ts
type SubFlowFunctionSchema = ZodTypeAny | Struct | Record<string, PlainValue>;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""A runtime function whose run block returns another function instead of raw data.

Hercules registers the returned ``run`` internally and reports it to Aquila as a sub
flow function, so the flow can invoke it later. Name, description and the I/O schema
are declared here because nothing in the module definition describes a function that
is only created at runtime. The Pydantic models are converted to JSON schemas while
the result is sent.
"""
from hercules import (
Description,
DisplayMessage,
FunctionContext,
Identifier,
Name,
Parameter,
RuntimeFunctionRunnable,
Signature,
SubFlowFunctionDefinition,
)
from hercules.schema import BaseModel, RootModel


class GreetInput(BaseModel):
"""The parameters the returned function accepts, keyed by parameter name."""

name: str


class GreetOutput(RootModel[str]):
"""The value the returned function produces."""


@Identifier("greeter_runtime")
@Signature("(greeting: STRING): TRANSFORM<STRING, STRING>")
@Name({"code": "en-US", "content": "Greeter"})
@DisplayMessage({"code": "en-US", "content": "Build a greeter saying ${greeting}"})
@Parameter(
{
"runtime_name": "greeting",
"name": [{"code": "en-US", "content": "Greeting"}],
"description": [
{
"code": "en-US",
"content": "The greeting the returned function prefixes a name with",
}
],
}
)
@Description({"code": "en-US", "content": "Returns a function that greets a name"})
class GreeterRuntimeFunction(RuntimeFunctionRunnable):
def run(self, context: FunctionContext, greeting: str) -> SubFlowFunctionDefinition:
return SubFlowFunctionDefinition(
name="greet",
description=f'Greets a name with "{greeting}"',
input_schema=GreetInput,
output_schema=GreetOutput,
run=lambda name: f"{greeting} {name}",
)
4 changes: 4 additions & 0 deletions py/examples/simple-example-py/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from functions.fibonacci_runtime_function import FibonacciRuntimeFunction
from functions.for_each_runtime_function import ForEachRuntimeFunction
from functions.for_each_consumers_runtime_function import ForEachConsumersRuntimeFunction
from functions.greeter_runtime_function import GreeterRuntimeFunction

action = Action(
os.environ.get("ACTION_ID", "testing-action"),
Expand Down Expand Up @@ -39,6 +40,9 @@
# Runtime function that takes a list of consumers (each an inline ${signature} sub flow reference).
action.register_runtime_function(ForEachConsumersRuntimeFunction)

# Runtime function that returns a sub flow function instead of raw data.
action.register_runtime_function(GreeterRuntimeFunction)

# Data type: derived from a schema.
action.register_data_type_class(EmailDataType)

Expand Down
13 changes: 11 additions & 2 deletions py/hercules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
RuntimeError,
RuntimeFlowTypeSetting_UniquenessScope,
SubFlow,
SubFlowFunctionDefinition,
SubFlowFunctionSchema,
Translation,
)
from hercules.events import CodeZeroEvent
Expand Down Expand Up @@ -55,6 +57,10 @@
from hercules.manager.function import FunctionManager
from hercules.manager.runtime_event import RuntimeEventManager
from hercules.manager.runtime_function import RuntimeFunctionManager
from hercules.manager.sub_flow_function import (
SUB_FLOW_FUNCTION_TTL_SECONDS,
SubFlowFunctionManager,
)
from hercules.models.datatype import DataTypeProps, DataTypeRunnable
from hercules.models.event import EventModel, EventSettingProps
from hercules.models.function import FunctionParameterProps, FunctionProps
Expand All @@ -63,7 +69,8 @@
from hercules.action import Action, is_export_mode, registered_actions

__all__ = [
"Translation", "PlainValue", "SubFlow", "FunctionContext",
"Translation", "PlainValue", "SubFlow", "SubFlowFunctionDefinition",
"SubFlowFunctionSchema", "FunctionContext",
"ProjectConfiguration", "ConfigurationDefinition", "RuntimeError",
"FlowTypeSetting_UniquenessScope", "RuntimeFlowTypeSetting_UniquenessScope",
"CodeZeroEvent", "SchemaType",
Expand All @@ -73,7 +80,9 @@
"OmitFunction", "Design", "ThrowsError", "Parameter", "Schema", "TypeString",
"EventSetting", "OmitEvent", "OmitRuntimeFunction",
"BaseManager", "ConfigManager", "FlowManager", "FunctionManager",
"RuntimeFunctionManager", "DataTypeManager", "EventManager", "RuntimeEventManager",
"RuntimeFunctionManager", "SubFlowFunctionManager",
"SUB_FLOW_FUNCTION_TTL_SECONDS", "DataTypeManager",
"EventManager", "RuntimeEventManager",
"FunctionProps", "FunctionParameterProps", "RuntimeFunctionProps",
"RuntimeFunctionRunnable", "DataTypeProps", "DataTypeRunnable", "EventModel",
"EventSettingProps", "RuntimeEventProps", "RuntimeEventRunnable",
Expand Down
Loading
Loading