feat: deep-agent canvas, live observability, and multi-environment tooling
Self-hosted platform for building, testing, and shipping LangChain/LangGraph agents. Deep-agent sub-agents on the canvas, a live tracing/observability timeline, auto-provisioned built-in tools with import/export, per-environment tool variables, streamed evaluations, and per-user auth token forwarding.
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/auth_provider",
|
||||
"type": "object",
|
||||
"required": ["name", "kind"],
|
||||
"properties": {
|
||||
"name": { "$ref": "forge/common#/$defs/Identifier" },
|
||||
"kind": { "type": "string", "enum": ["csrf_session", "oauth2_client_credentials", "oauth2_authorization_code", "bearer", "basic", "api_key", "custom_script"], "x-ui": { "widget": "segmented" } },
|
||||
"credentials_ref": { "$ref": "forge/common#/$defs/SecretRef" },
|
||||
"cache_ttl_seconds": { "type": "integer", "default": 1800 },
|
||||
"refresh_on": { "type": "array", "items": { "type": "integer" }, "default": [401, 403] },
|
||||
"per_user_context_keys": { "type": "array", "items": { "type": "string" }, "description": "Context keys (e.g. csrf, session from the widget) that vary the cache key and feed templates as {{ctx.*}}." },
|
||||
"token_ctx_key": { "type": "string", "description": "Per-user bearer/api_key only: a run-context key (X-Forge-Context) whose value is used as the token when present, taking precedence over the stored per-user connection. Lets a server-to-server caller (e.g. a web chat) forward the acting user's token inline while MCP/console fall back to the stored credential." },
|
||||
"extra_headers": { "type": "object", "additionalProperties": { "type": "string" }, "x-ui": { "widget": "rows-table", "section": "Extra headers" }, "description": "Fixed headers stamped on every outbound call in addition to the primary auth header. Each value is a literal OR a secret:// ref (resolved from the secret store). Define shared service/attestation headers (e.g. X-Forge-Client-Id, a service token) once here instead of hardcoding them per tool." }
|
||||
},
|
||||
"allOf": [
|
||||
{ "if": { "properties": { "kind": { "const": "csrf_session" } } }, "then": { "$ref": "#/$defs/FetchExtractInject" } },
|
||||
{ "if": { "properties": { "kind": { "const": "oauth2_client_credentials" } } }, "then": { "$ref": "#/$defs/OAuth2" } },
|
||||
{ "if": { "properties": { "kind": { "const": "oauth2_authorization_code" } } }, "then": { "$ref": "#/$defs/OAuth2AuthCode" } },
|
||||
{ "if": { "properties": { "kind": { "const": "bearer" } } }, "then": { "$ref": "#/$defs/Bearer" } },
|
||||
{ "if": { "properties": { "kind": { "const": "basic" } } }, "then": { "$ref": "#/$defs/Basic" } },
|
||||
{ "if": { "properties": { "kind": { "const": "api_key" } } }, "then": { "$ref": "#/$defs/ApiKey" } },
|
||||
{ "if": { "properties": { "kind": { "const": "custom_script" } } }, "then": { "$ref": "#/$defs/CustomScript" } }
|
||||
],
|
||||
"$defs": {
|
||||
"ExtractRule": {
|
||||
"type": "object",
|
||||
"required": ["name", "from"],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"from": { "type": "string", "enum": ["header", "cookie", "json"] },
|
||||
"header": { "type": "string" },
|
||||
"cookie": { "type": "string" },
|
||||
"json_path": { "type": "string" },
|
||||
"kind": { "type": "string", "enum": ["value", "ttl"], "default": "value" }
|
||||
}
|
||||
},
|
||||
"InjectRule": {
|
||||
"type": "object",
|
||||
"required": ["to", "name", "value"],
|
||||
"properties": {
|
||||
"to": { "type": "string", "enum": ["header", "cookie", "query"] },
|
||||
"name": { "type": "string" },
|
||||
"value": { "$ref": "forge/common#/$defs/TemplateString", "x-ui": { "widget": "token-template", "tokens": ["extracted"] } }
|
||||
}
|
||||
},
|
||||
"FetchExtractInject": {
|
||||
"required": ["token_fetch", "extract", "inject"],
|
||||
"properties": {
|
||||
"token_fetch": {
|
||||
"type": "object",
|
||||
"required": ["method", "url"],
|
||||
"x-ui": { "section": "Token fetch" },
|
||||
"properties": {
|
||||
"method": { "type": "string", "enum": ["GET", "POST"] },
|
||||
"url": { "type": "string" },
|
||||
"headers": { "type": "object", "x-ui": { "widget": "token-template", "tokens": ["cred", "ctx"] } },
|
||||
"body": { "type": "object", "x-ui": { "widget": "token-template", "tokens": ["cred", "ctx"] } }
|
||||
}
|
||||
},
|
||||
"extract": { "type": "array", "items": { "$ref": "#/$defs/ExtractRule" }, "x-ui": { "widget": "rows-table", "section": "Extract" } },
|
||||
"inject": { "type": "array", "items": { "$ref": "#/$defs/InjectRule" }, "x-ui": { "widget": "rows-table", "section": "Inject" } }
|
||||
}
|
||||
},
|
||||
"OAuth2": {
|
||||
"required": ["token_url"],
|
||||
"properties": {
|
||||
"token_url": { "type": "string" },
|
||||
"scope": { "type": "string" },
|
||||
"client_id_ref": { "$ref": "forge/common#/$defs/SecretRef" },
|
||||
"client_secret_ref": { "$ref": "forge/common#/$defs/SecretRef" },
|
||||
"audience": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"OAuth2AuthCode": {
|
||||
"required": ["authorize_url", "token_url"],
|
||||
"properties": {
|
||||
"authorize_url": { "type": "string", "x-ui": { "help": "Provider's authorization endpoint (the user is redirected here to grant access)." } },
|
||||
"token_url": { "type": "string" },
|
||||
"scope": { "type": "string" },
|
||||
"client_id_ref": { "$ref": "forge/common#/$defs/SecretRef" },
|
||||
"client_secret_ref": { "$ref": "forge/common#/$defs/SecretRef" },
|
||||
"redirect_uri": { "type": "string", "description": "Optional override; defaults to {oauth_redirect_base}/v1/oauth/callback." },
|
||||
"header_name": { "type": "string", "default": "Authorization" },
|
||||
"prefix": { "type": "string", "default": "Bearer " },
|
||||
"token_bundle_ref": { "$ref": "forge/common#/$defs/SecretRef", "description": "Where the connected access/refresh tokens are stored (set by the connect flow)." }
|
||||
}
|
||||
},
|
||||
"Bearer": {
|
||||
"properties": {
|
||||
"token_ref": { "$ref": "forge/common#/$defs/SecretRef" },
|
||||
"header_name": { "type": "string", "default": "Authorization" },
|
||||
"prefix": { "type": "string", "default": "Bearer " }
|
||||
}
|
||||
},
|
||||
"Basic": {
|
||||
"properties": {
|
||||
"username_ref": { "$ref": "forge/common#/$defs/SecretRef" },
|
||||
"password_ref": { "$ref": "forge/common#/$defs/SecretRef" }
|
||||
}
|
||||
},
|
||||
"ApiKey": {
|
||||
"required": ["in", "name"],
|
||||
"properties": {
|
||||
"in": { "type": "string", "enum": ["header", "query"] },
|
||||
"name": { "type": "string" },
|
||||
"value_ref": { "$ref": "forge/common#/$defs/SecretRef" }
|
||||
}
|
||||
},
|
||||
"CustomScript": {
|
||||
"required": ["source"],
|
||||
"properties": {
|
||||
"source": { "type": "string", "x-ui": { "widget": "code", "language": "python", "help": "RestrictedPython returning {headers, cookies, ttl}. Advanced - audited." } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/common",
|
||||
"$defs": {
|
||||
"IOType": {
|
||||
"type": "string",
|
||||
"enum": ["messages", "text", "json", "tool", "embedding", "vector", "any", "control"],
|
||||
"description": "Port data type. Connection valid iff source/target compatible; 'any' matches all; 'control' only connects to 'control'."
|
||||
},
|
||||
"Port": {
|
||||
"type": "object",
|
||||
"required": ["id", "io_type", "direction"],
|
||||
"properties": {
|
||||
"id": { "type": "string", "pattern": "^[a-z0-9_]+$" },
|
||||
"label": { "type": "string" },
|
||||
"io_type": { "$ref": "forge/common#/$defs/IOType" },
|
||||
"direction": { "type": "string", "enum": ["in", "out"] },
|
||||
"required": { "type": "boolean", "default": true },
|
||||
"many": { "type": "boolean", "default": false, "description": "Accepts/produces multiple connections (e.g. fanout/join control ports)." }
|
||||
}
|
||||
},
|
||||
"ModelRef": {
|
||||
"type": "string",
|
||||
"description": "Provider-prefixed model id, e.g. 'anthropic:claude-sonnet-4-6', 'openai:gpt-5.4', 'google_genai:gemini-3.1-pro-preview', or a gateway 'litellm:...'/'openrouter:...'.",
|
||||
"x-ui": { "widget": "model-picker" }
|
||||
},
|
||||
"ContextSize": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"maxItems": 2,
|
||||
"prefixItems": [
|
||||
{ "type": "string", "enum": ["tokens", "messages", "fraction"] },
|
||||
{ "type": "number" }
|
||||
],
|
||||
"description": "LangChain ContextSize tuple, e.g. ['tokens',4000] or ['fraction',0.8]."
|
||||
},
|
||||
"TemplateString": {
|
||||
"type": "string",
|
||||
"description": "May contain {{...}} token references resolved at runtime."
|
||||
},
|
||||
"Identifier": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z0-9_-]+$",
|
||||
"description": "snake_case/kebab; no spaces (provider compatibility)."
|
||||
},
|
||||
"SecretRef": {
|
||||
"type": "string",
|
||||
"pattern": "^(secret|vault)://.+",
|
||||
"description": "Reference to a stored secret; never a plaintext value.",
|
||||
"x-ui": { "widget": "secret-ref" }
|
||||
},
|
||||
"Expression": {
|
||||
"type": "string",
|
||||
"description": "RestrictedPython expression evaluated over the run state/context. No imports, no attribute escapes."
|
||||
},
|
||||
"RetryPolicy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"max_retries": { "type": "integer", "minimum": 0, "default": 2 },
|
||||
"backoff_factor": { "type": "number", "default": 2.0 },
|
||||
"initial_delay": { "type": "number", "default": 1.0 },
|
||||
"max_delay": { "type": "number", "default": 60.0 },
|
||||
"jitter": { "type": "boolean", "default": true },
|
||||
"retry_on": { "type": "array", "items": { "type": "string" }, "description": "Failure kinds to retry: exception, timeout, connection, http_error, value_error, key_error, runtime_error. Empty = retry any exception." }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/middleware",
|
||||
"$defs": {
|
||||
"MiddlewareEntry": {
|
||||
"type": "object",
|
||||
"required": ["type"],
|
||||
"properties": {
|
||||
"type": { "type": "string", "description": "Middleware type key (see config_schemas)." },
|
||||
"enabled": { "type": "boolean", "default": true },
|
||||
"config": { "type": "object", "description": "Validated against the per-type schema in config_schemas." }
|
||||
},
|
||||
"x-ui": {
|
||||
"category-map": {
|
||||
"summarization": "context", "context_editing": "context", "todo": "context",
|
||||
"pii": "safety", "guardrail_regex": "safety", "openai_moderation": "safety",
|
||||
"tool_retry": "reliability", "model_retry": "reliability", "model_fallback": "reliability",
|
||||
"model_call_limit": "cost", "tool_call_limit": "cost", "tenant_budget": "cost", "llm_tool_selector": "cost",
|
||||
"semantic_cache": "cost",
|
||||
"human_in_the_loop": "human",
|
||||
"anthropic_prompt_caching": "provider",
|
||||
"tool_emulator": "testing",
|
||||
"dynamic_model_by_state": "advanced", "tool_filter_by_context": "advanced"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"config_schemas": {
|
||||
"summarization": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": { "$ref": "forge/common#/$defs/ModelRef" },
|
||||
"trigger": { "oneOf": [{ "$ref": "forge/common#/$defs/ContextSize" }, { "type": "array", "items": { "$ref": "forge/common#/$defs/ContextSize" } }] },
|
||||
"keep": { "$ref": "forge/common#/$defs/ContextSize" },
|
||||
"summary_prompt": { "type": "string", "x-ui": { "widget": "textarea", "advanced": true } }
|
||||
},
|
||||
"x-ui": { "help": "Summarize older messages when the conversation grows past the trigger; keep the most recent." }
|
||||
},
|
||||
"human_in_the_loop": {
|
||||
"type": "object",
|
||||
"required": ["interrupt_on"],
|
||||
"properties": {
|
||||
"interrupt_on": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"oneOf": [
|
||||
{ "type": "boolean" },
|
||||
{ "type": "object", "properties": { "allowed_decisions": { "type": "array", "items": { "type": "string", "enum": ["approve", "edit", "reject"] } } } }
|
||||
]
|
||||
},
|
||||
"x-ui": { "widget": "rows-table", "columns": ["tool", "allowed_decisions"] }
|
||||
}
|
||||
},
|
||||
"x-ui": { "help": "Pause for human approval before chosen tools run. Requires a checkpointer." }
|
||||
},
|
||||
"model_call_limit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"thread_limit": { "type": "integer" },
|
||||
"run_limit": { "type": "integer" },
|
||||
"exit_behavior": { "type": "string", "enum": ["end", "error"], "default": "end" }
|
||||
}
|
||||
},
|
||||
"tool_call_limit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tool_name": { "type": "string", "x-ui": { "widget": "reference", "refType": "tool" } },
|
||||
"thread_limit": { "type": "integer" },
|
||||
"run_limit": { "type": "integer" },
|
||||
"exit_behavior": { "type": "string", "enum": ["continue", "error", "end"], "default": "continue" }
|
||||
}
|
||||
},
|
||||
"model_fallback": {
|
||||
"type": "object",
|
||||
"required": ["models"],
|
||||
"properties": {
|
||||
"models": { "type": "array", "items": { "$ref": "forge/common#/$defs/ModelRef" }, "minItems": 1, "x-ui": { "widget": "chips", "help": "Tried in order if the primary model fails." } }
|
||||
}
|
||||
},
|
||||
"pii": {
|
||||
"type": "object",
|
||||
"required": ["pii_type"],
|
||||
"properties": {
|
||||
"pii_type": { "type": "string", "description": "email|credit_card|ip|mac_address|url|<custom>" },
|
||||
"strategy": { "type": "string", "enum": ["block", "redact", "mask", "hash"], "default": "redact" },
|
||||
"detector": { "type": "string", "description": "Regex pattern for custom types.", "x-ui": { "widget": "mono", "advanced": true } },
|
||||
"apply_to_input": { "type": "boolean", "default": true },
|
||||
"apply_to_output": { "type": "boolean", "default": false },
|
||||
"apply_to_tool_results": { "type": "boolean", "default": false }
|
||||
}
|
||||
},
|
||||
"todo": {
|
||||
"type": "object",
|
||||
"properties": { "system_prompt": { "type": "string", "x-ui": { "widget": "textarea", "advanced": true } } }
|
||||
},
|
||||
"llm_tool_selector": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": { "$ref": "forge/common#/$defs/ModelRef" },
|
||||
"max_tools": { "type": "integer" },
|
||||
"always_include": { "type": "array", "items": { "type": "string" }, "x-ui": { "widget": "chips", "refType": "tool" } }
|
||||
},
|
||||
"x-ui": { "help": "Pre-select relevant tools before the main model call (saves tokens when many tools exist)." }
|
||||
},
|
||||
"tool_retry": {
|
||||
"allOf": [{ "$ref": "forge/common#/$defs/RetryPolicy" }],
|
||||
"properties": {
|
||||
"tools": { "type": "array", "items": { "type": "string" }, "x-ui": { "widget": "chips", "refType": "tool" } },
|
||||
"on_failure": { "type": "string", "enum": ["return_message", "raise", "continue"], "default": "return_message" }
|
||||
}
|
||||
},
|
||||
"model_retry": {
|
||||
"allOf": [{ "$ref": "forge/common#/$defs/RetryPolicy" }],
|
||||
"properties": { "on_failure": { "type": "string", "enum": ["continue", "error"], "default": "continue" } }
|
||||
},
|
||||
"tool_emulator": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tools": { "type": "array", "items": { "type": "string" }, "x-ui": { "widget": "chips", "refType": "tool" } },
|
||||
"model": { "$ref": "forge/common#/$defs/ModelRef" }
|
||||
},
|
||||
"x-ui": { "help": "Emulate tool outputs with an LLM (no real calls). For testing." }
|
||||
},
|
||||
"context_editing": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"edits": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"trigger": { "type": "integer", "default": 100000 },
|
||||
"keep": { "type": "integer", "default": 3 },
|
||||
"clear_at_least": { "type": "integer", "default": 0 },
|
||||
"clear_tool_inputs": { "type": "boolean", "default": false },
|
||||
"exclude_tools": { "type": "array", "items": { "type": "string" } },
|
||||
"placeholder": { "type": "string", "default": "[cleared]" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-ui": { "help": "Clear older tool outputs once the conversation passes the trigger; keep the most recent N." }
|
||||
},
|
||||
"anthropic_prompt_caching": {
|
||||
"type": "object",
|
||||
"properties": { "ttl": { "type": "string", "enum": ["5m", "1h"], "default": "5m" } },
|
||||
"x-ui": { "help": "Cache the system prompt block on Anthropic models (cost/latency).", "showWhen": "model.startsWith(anthropic)" }
|
||||
},
|
||||
"openai_moderation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apply_to_input": { "type": "boolean", "default": true },
|
||||
"apply_to_output": { "type": "boolean", "default": true }
|
||||
}
|
||||
},
|
||||
"dynamic_model_by_state": {
|
||||
"type": "object",
|
||||
"required": ["rules"],
|
||||
"properties": {
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": { "type": "object", "properties": { "when": { "$ref": "forge/common#/$defs/Expression" }, "use": { "$ref": "forge/common#/$defs/ModelRef" } } },
|
||||
"x-ui": { "widget": "rows-table" }
|
||||
},
|
||||
"default": { "$ref": "forge/common#/$defs/ModelRef" }
|
||||
},
|
||||
"x-ui": { "help": "Switch model at runtime based on state (compiles to @wrap_model_call)." }
|
||||
},
|
||||
"tool_filter_by_context": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expose_when": { "type": "object", "description": "e.g. {role:['admin']} matched against runtime.context." },
|
||||
"tools": { "type": "array", "items": { "type": "string" }, "x-ui": { "widget": "chips", "refType": "tool" } }
|
||||
},
|
||||
"x-ui": { "help": "Show/hide tools at runtime by context (auth state, role, flags)." }
|
||||
},
|
||||
"guardrail_regex": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"patterns": { "type": "array", "items": { "type": "string" }, "x-ui": { "widget": "chips" } },
|
||||
"on_match": { "type": "string", "enum": ["block", "redact", "flag"], "default": "block" },
|
||||
"apply_to": { "type": "string", "enum": ["input", "output", "both"], "default": "output" }
|
||||
}
|
||||
},
|
||||
"tenant_budget": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"max_usd_per_thread": { "type": "number" },
|
||||
"max_tokens_per_run": { "type": "integer" },
|
||||
"on_exceed": { "type": "string", "enum": ["end", "error"], "default": "end" }
|
||||
},
|
||||
"x-ui": { "help": "Stop the run when accumulated cost/tokens exceed the cap (before_model)." }
|
||||
},
|
||||
"semantic_cache": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"threshold": { "type": "number", "minimum": 0, "maximum": 1, "default": 0.95, "description": "Minimum cosine similarity for a cached answer to be reused. High by design - a wrong cache hit is worse than a miss." },
|
||||
"ttl": { "type": "integer", "default": 3600, "description": "Seconds a cached answer stays valid." },
|
||||
"scope": { "type": "string", "default": "default", "description": "Cache partition (e.g. per-locale/per-tenant-segment); questions only match within the same scope." },
|
||||
"min_question_chars": { "type": "integer", "default": 8, "description": "Skip caching for questions shorter than this (too generic to key on)." }
|
||||
},
|
||||
"x-ui": { "help": "Reuse a previous answer when a semantically-equivalent question was already answered - skips the model/tool loop entirely on a hit." }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/nodes/agent",
|
||||
"type": "object",
|
||||
"anyOf": [{ "required": ["model"] }, { "required": ["agent_ref"] }],
|
||||
"properties": {
|
||||
"agent_ref": { "type": "string", "description": "Id of a saved Agent preset this node mirrors. When set, the saved agent's config drives this node (live - edits in the Agents tab apply).", "x-ui": { "widget": "reference", "refType": "agent", "section": "Flavor", "order": 0 } },
|
||||
"flavor": { "type": "string", "enum": ["agent", "deep_agent"], "default": "agent", "x-ui": { "widget": "segmented", "order": 1, "section": "Flavor" } },
|
||||
"name": { "$ref": "forge/common#/$defs/Identifier", "x-ui": { "section": "Flavor" } },
|
||||
"description": { "type": "string", "description": "When this agent is used as a deep-agent sub-agent, the supervisor reads this to decide when to call it (like a tool description).", "x-ui": { "section": "Flavor", "widget": "textarea" } },
|
||||
"model": { "$ref": "forge/common#/$defs/ModelRef", "x-ui": { "section": "Model", "order": 2 } },
|
||||
"model_params": {
|
||||
"type": "object",
|
||||
"x-ui": { "section": "Model", "advanced": true },
|
||||
"properties": {
|
||||
"temperature": { "type": "number" },
|
||||
"max_tokens": { "type": "integer" },
|
||||
"timeout": { "type": "integer" },
|
||||
"base_url": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"dynamic_model": {
|
||||
"type": "object",
|
||||
"x-ui": { "section": "Model", "advanced": true },
|
||||
"description": "Compiles to a @wrap_model_call middleware.",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean", "default": false },
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": { "type": "object", "required": ["when", "use"], "properties": { "when": { "$ref": "forge/common#/$defs/Expression" }, "use": { "$ref": "forge/common#/$defs/ModelRef" } } },
|
||||
"x-ui": { "widget": "rows-table" }
|
||||
},
|
||||
"default": { "$ref": "forge/common#/$defs/ModelRef" }
|
||||
}
|
||||
},
|
||||
"system_prompt": { "type": "string", "x-ui": { "widget": "textarea", "section": "Instructions", "order": 3, "modelView": true } },
|
||||
"dynamic_prompt": {
|
||||
"type": "object",
|
||||
"x-ui": { "section": "Instructions", "advanced": true },
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": { "type": "object", "properties": { "when": { "$ref": "forge/common#/$defs/Expression" }, "prompt": { "type": "string" } } },
|
||||
"x-ui": { "widget": "rows-table" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"tools": { "type": "array", "items": { "type": "string" }, "description": "Tool ids (incl. MCP tool ids) available to the agent.", "x-ui": { "widget": "chips", "refType": "tool", "section": "Tools", "order": 4 } },
|
||||
"toolsets": { "type": "array", "items": { "type": "string" }, "description": "Tool set ids granted to the agent - every tool in each set becomes available (in addition to any individually-listed tools).", "x-ui": { "widget": "chips", "refType": "tool_set", "section": "Tools", "order": 4.5 } },
|
||||
"knowledge": {
|
||||
"type": "object",
|
||||
"description": "Built-in knowledge-base access for the agent (no separate Tool needed). Compiles to agent-callable tools: RAG over documents and/or curated Q&A lookup, each scoped by folders/kinds. Prefer this over a fixed retrieval node for conversational or multi-part agents.",
|
||||
"x-ui": { "section": "Knowledge", "order": 5 },
|
||||
"properties": {
|
||||
"rag": {
|
||||
"type": "object",
|
||||
"description": "Document search (RAG) as a tool the agent calls per sub-question.",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean", "default": false },
|
||||
"folders": { "type": "array", "items": { "type": "string" }, "x-ui": { "widget": "chips", "refType": "kb_folder" }, "description": "Restrict to these knowledge folders (empty = all)." },
|
||||
"top_k": { "type": "integer", "default": 4, "description": "Document chunks returned per search." },
|
||||
"min_score": { "type": "number", "default": 0.18, "description": "Drop chunks below this cosine similarity so off-topic queries surface nothing. Not applied when rerank is on (the cross-encoder score is on a different scale)." },
|
||||
"hybrid": { "type": "boolean", "default": false, "description": "Blend lexical BM25 keyword ranking with vector search (RRF) so exact terms - codes, names, SKUs - aren't missed." },
|
||||
"rerank": { "type": "boolean", "default": false, "description": "Two-stage retrieval: a local cross-encoder re-scores the shortlist and keeps only the best matches. Big accuracy boost; adds some latency. Runs offline on CPU (no extra cost)." },
|
||||
"rerank_top_n": { "type": "integer", "description": "How many chunks to pull into the rerank shortlist before the cross-encoder trims them to top_k (only when rerank is on). Leave unset to auto-size (roughly 6× top_k, minimum 25)." }
|
||||
}
|
||||
},
|
||||
"qa": {
|
||||
"type": "object",
|
||||
"description": "FAQ / Q&A lookup as a tool the agent calls for common questions.",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean", "default": false },
|
||||
"kinds": { "type": "array", "items": { "type": "string" }, "x-ui": { "widget": "chips", "refType": "qa_kind" }, "description": "Restrict to these Q&A kinds/categories (empty = all)." },
|
||||
"threshold": { "type": "number", "default": 0.3, "description": "Minimum similarity for a Q&A pair to be returned." },
|
||||
"top_k": { "type": "integer", "default": 3, "description": "Q&A pairs returned per lookup." }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mcp_servers": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Registered MCP servers (by id) this agent can use - each server's enabled tools become agent-callable.",
|
||||
"x-ui": { "widget": "chips", "refType": "mcp_client", "section": "MCP Servers", "order": 5.5 }
|
||||
},
|
||||
"response_format": { "$ref": "#/$defs/ResponseFormat", "x-ui": { "section": "Output", "order": 6 } },
|
||||
"middleware": { "type": "array", "items": { "$ref": "forge/middleware#/$defs/MiddlewareEntry" }, "x-ui": { "widget": "middleware-stack", "section": "Middleware", "order": 6 } },
|
||||
"memory": {
|
||||
"type": "object",
|
||||
"x-ui": { "section": "Memory", "order": 7 },
|
||||
"properties": {
|
||||
"long_term": { "type": "boolean", "default": false, "x-ui": { "effect": "Persist memory across conversations" } },
|
||||
"store_namespace": { "type": "string" },
|
||||
"state_extensions": {
|
||||
"type": "array",
|
||||
"items": { "type": "object", "properties": { "name": { "type": "string" }, "type": { "type": "string" }, "reducer": { "type": "string" } } },
|
||||
"x-ui": { "widget": "rows-table" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"planning": { "type": "boolean", "default": true, "x-ui": { "section": "Deep Agent", "order": 8, "effect": "Adds write_todos planning tool", "showWhen": "flavor=deep_agent" } },
|
||||
"subagents": { "type": "array", "items": { "$ref": "#/$defs/SubAgent" }, "x-ui": { "section": "Deep Agent", "order": 9, "showWhen": "flavor=deep_agent" } },
|
||||
"filesystem": { "$ref": "#/$defs/FilesystemBackend", "x-ui": { "section": "Deep Agent", "order": 10, "showWhen": "flavor=deep_agent" } },
|
||||
"sandbox": { "$ref": "#/$defs/SandboxConfig", "x-ui": { "section": "Deep Agent", "order": 11, "showWhen": "flavor=deep_agent" } },
|
||||
"skills": { "type": "array", "items": { "type": "string" }, "x-ui": { "section": "Deep Agent", "showWhen": "flavor=deep_agent" } },
|
||||
"permissions": {
|
||||
"type": "array",
|
||||
"items": { "type": "object", "properties": { "path": { "type": "string" }, "access": { "type": "string", "enum": ["read", "write", "none"] } } },
|
||||
"x-ui": { "widget": "rows-table", "section": "Deep Agent", "showWhen": "flavor=deep_agent" }
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"ResponseFormat": {
|
||||
"oneOf": [
|
||||
{ "type": "object", "properties": { "mode": { "const": "freeform" } } },
|
||||
{ "type": "object", "required": ["mode", "schema"], "properties": { "mode": { "const": "structured" }, "strategy": { "type": "string", "enum": ["auto", "provider", "tool"], "default": "auto" }, "schema": { "type": "object", "description": "JSON Schema of the desired structured output." } } }
|
||||
],
|
||||
"x-ui": { "widget": "kind-switcher", "discriminator": "mode" }
|
||||
},
|
||||
"SubAgent": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["name", "description"],
|
||||
"properties": {
|
||||
"name": { "$ref": "forge/common#/$defs/Identifier" },
|
||||
"description": { "type": "string", "x-ui": { "modelView": true } },
|
||||
"system_prompt": { "type": "string", "x-ui": { "widget": "textarea" } },
|
||||
"tools": { "type": "array", "items": { "type": "string" }, "x-ui": { "widget": "chips", "refType": "tool" } },
|
||||
"toolsets": { "type": "array", "items": { "type": "string" }, "x-ui": { "widget": "chips", "refType": "tool_set" } },
|
||||
"model": { "$ref": "forge/common#/$defs/ModelRef" },
|
||||
"middleware": { "type": "array", "items": { "$ref": "forge/middleware#/$defs/MiddlewareEntry" } }
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["name", "workflow_ref"],
|
||||
"properties": {
|
||||
"name": { "$ref": "forge/common#/$defs/Identifier" },
|
||||
"description": { "type": "string" },
|
||||
"workflow_ref": { "type": "object", "properties": { "workflow_id": { "type": "string" }, "version": { "type": "integer" } }, "description": "Wrap a compiled workflow as CompiledSubAgent.", "x-ui": { "widget": "reference", "refType": "workflow" } }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"FilesystemBackend": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "enum": ["state", "disk", "store", "composite"], "default": "state" },
|
||||
"routes": { "type": "object", "additionalProperties": { "type": "string", "enum": ["state", "disk", "store"] }, "description": "For composite, e.g. {'/memories/':'store'}." }
|
||||
}
|
||||
},
|
||||
"SandboxConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "enum": ["none", "docker", "remote"], "default": "none" },
|
||||
"provider": { "type": "string", "enum": ["e2b", "modal", "daytona", "runloop", "agentcore"], "x-ui": { "showWhen": "kind=remote" } },
|
||||
"pattern": { "type": "string", "enum": ["agent_in_sandbox", "sandbox_as_tool"], "default": "sandbox_as_tool" },
|
||||
"limits": { "$ref": "#/$defs/Limits" }
|
||||
}
|
||||
},
|
||||
"Limits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cpu_seconds": { "type": "integer", "default": 10 },
|
||||
"memory_mb": { "type": "integer", "default": 512 },
|
||||
"timeout_seconds": { "type": "integer", "default": 30 },
|
||||
"egress_allowlist": { "type": "array", "items": { "type": "string" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/nodes/classifier",
|
||||
"type": "object",
|
||||
"required": ["labels"],
|
||||
"properties": {
|
||||
"labels": { "type": "array", "items": { "type": "string" }, "minItems": 1, "description": "The intent labels to classify into (e.g. return_item, cancel_subscription, get_information)." },
|
||||
"output_key": { "type": "string", "default": "intent", "description": "State field the chosen label is written to - a Router downstream branches on it." },
|
||||
"multi_label": { "type": "boolean", "default": false, "x-ui": { "effect": "Write EVERY applicable label (a list) instead of exactly one - pair with a Router set to 'multi' so a question with several intents fans out to every matching branch in parallel." } },
|
||||
"model": { "$ref": "forge/common#/$defs/ModelRef", "description": "Empty = project default model." },
|
||||
"instructions": { "type": "string", "description": "Optional extra guidance for the classification (domain hints, tie-breaking rules)." }
|
||||
},
|
||||
"$comment": "ports: in:text, out:text. Routing pattern: classifier -> router(expression=output_key, cases per label). multi_label writes list[str]."
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/nodes/emit_event",
|
||||
"type": "object",
|
||||
"required": ["channel"],
|
||||
"properties": {
|
||||
"channel": { "type": "string" },
|
||||
"payload": { "type": "object" }
|
||||
},
|
||||
"$comment": "ports: in:any, out:any (pushes a custom SSE frame)"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/nodes/end",
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false,
|
||||
"$comment": "ports: in:control"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/nodes/handoff",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reason": { "type": "string", "default": "Escalated to a human agent.", "x-ui": { "help": "Why the conversation is being handed off (shown in the agent inbox)." } },
|
||||
"ack_message": { "type": "string", "x-ui": { "help": "Optional message sent to the customer while they wait for a human." } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/nodes/human_input",
|
||||
"type": "object",
|
||||
"required": ["prompt"],
|
||||
"properties": {
|
||||
"prompt": { "type": "string", "x-ui": { "widget": "textarea" } },
|
||||
"schema": { "type": "object", "description": "Optional structured input schema requested from the human." },
|
||||
"allowed_decisions": { "type": "array", "items": { "type": "string", "enum": ["approve", "edit", "reject"] }, "default": ["approve", "reject"] },
|
||||
"timeout_seconds": { "type": "integer", "minimum": 1, "description": "Optional approval deadline in seconds (overrides the global HITL timeout)." },
|
||||
"timeout_default": { "type": "string", "enum": ["approve", "edit", "reject"], "description": "Decision used to resume automatically when the approval deadline expires. Leave empty to fail the run instead." },
|
||||
"output_key": { "type": "string", "description": "Optional state key the decision string is written to, so a Router can branch approve → continue / reject → end." }
|
||||
},
|
||||
"$comment": "ports: in:any, out:any (calls interrupt(); requires checkpointer)"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/nodes/join",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reducer": { "type": "string", "enum": ["concat", "merge", "first", "last"], "default": "concat" },
|
||||
"input_key": { "type": "string", "description": "State key holding the fan-out results to re-aggregate. Omit to use the join purely as a convergence marker (results are then aggregated by that key's own add-reducer)." },
|
||||
"output_key": { "type": "string", "description": "State key to write the aggregated result to (defaults to input_key). Point at a 'last'-reducer field to overwrite cleanly rather than append." }
|
||||
},
|
||||
"$comment": "ports: in:control[], out:json"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/nodes/llm",
|
||||
"type": "object",
|
||||
"required": ["model", "prompt"],
|
||||
"properties": {
|
||||
"model": { "$ref": "forge/common#/$defs/ModelRef" },
|
||||
"prompt": { "$ref": "forge/common#/$defs/TemplateString", "x-ui": { "widget": "textarea", "modelView": true } },
|
||||
"model_params": { "type": "object" },
|
||||
"response_format": { "$ref": "forge/nodes/agent#/$defs/ResponseFormat" }
|
||||
},
|
||||
"$comment": "ports: in:[text,json], out:[text,json]"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/nodes/loop",
|
||||
"type": "object",
|
||||
"required": ["condition", "max_iter"],
|
||||
"properties": {
|
||||
"condition": { "$ref": "forge/common#/$defs/Expression" },
|
||||
"max_iter": { "type": "integer", "default": 10 }
|
||||
},
|
||||
"$comment": "ports: in:any, out:any (allows_cycle = true)"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/nodes/parallel_fanout",
|
||||
"type": "object",
|
||||
"required": ["over", "child_node", "item_key"],
|
||||
"properties": {
|
||||
"over": { "type": "string", "description": "State key holding a list." },
|
||||
"child_node": { "type": "string", "description": "Node id to run per item via Send." },
|
||||
"item_key": { "type": "string", "description": "State key the item is written to for each child." },
|
||||
"index_key": { "type": "string", "default": "_fanout_index", "description": "Key each child receives its 0-based input index under (also gets _fanout_total), so results can be reordered deterministically despite parallel completion order." },
|
||||
"on_item_error": { "type": "string", "enum": ["fail", "skip"], "default": "fail", "description": "fail: one item's error aborts the whole batch (default). skip: isolate it - log and drop that item's contribution, keep the rest. (Also enabled by workflow error_policy 'continue'.)" },
|
||||
"item_timeout_seconds": { "type": "number", "description": "Optional per-item timeout. Applies to async children / subgraphs (sync children can't be preempted). A timed-out item follows on_item_error." }
|
||||
},
|
||||
"$comment": "ports: in:json, out:control[]"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/nodes/retrieval",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"include_docs": { "type": "boolean", "default": true, "x-ui": { "effect": "Search the knowledge-base DOCUMENTS (RAG). Turn off for a Q&A-only node that pulls only curated FAQ pairs." } },
|
||||
"top_k": { "type": "integer", "default": 5, "description": "How many knowledge-base document chunks to retrieve (only when include_docs is true)." },
|
||||
"hybrid": { "type": "boolean", "default": false, "x-ui": { "effect": "Hybrid search: blend lexical BM25 keyword matching with semantic vectors (RRF). Catches exact terms - codes, names, SKUs - that pure vector search can miss." } },
|
||||
"rerank": { "type": "boolean", "default": false, "x-ui": { "effect": "Two-stage retrieval: after the initial search, a local cross-encoder re-scores the shortlist and keeps only the best matches. Big accuracy boost; adds some latency. Runs offline on CPU (no extra cost)." } },
|
||||
"rerank_top_n": { "type": "integer", "description": "How many chunks to pull into the rerank shortlist before the cross-encoder trims them to top_k (only when rerank is true). Leave unset to auto-size the shortlist (roughly 6× top_k, minimum 25)." },
|
||||
"rerank_min_score": { "type": "number", "default": 0.3, "description": "Drop reranked chunks below this cross-encoder relevance (0-1 sigmoid scale) so an off-topic query still yields nothing when rerank is on. Only applied when rerank is true (min_score is a cosine floor on a different scale and is skipped then)." },
|
||||
"mmr": { "type": "boolean", "default": false, "x-ui": { "effect": "Diversify results (Maximal Marginal Relevance): trade a little relevance to avoid returning several near-duplicate chunks. Useful when one topic dominates the knowledge base." } },
|
||||
"mmr_lambda": { "type": "number", "default": 0.5, "description": "MMR relevance/diversity balance (0-1): 1.0 = pure relevance, lower = more diverse. Only applied when mmr is true." },
|
||||
"include_qa": { "type": "boolean", "default": false, "x-ui": { "effect": "Also pull matching Q&A pairs into context - retrieval then grounds the agent on BOTH KB docs and Q&A." } },
|
||||
"qa_threshold": { "type": "number", "default": 0.3, "description": "Minimum similarity for a Q&A pair to be included (only when include_qa is true)." },
|
||||
"qa_top_k": { "type": "integer", "default": 3, "description": "How many Q&A pairs to retrieve (only when include_qa is true)." },
|
||||
"qa_kinds": { "type": "array", "items": { "type": "string" }, "x-ui": { "widget": "chips", "refType": "qa_kind" }, "description": "Only include Q&A pairs of these kinds/categories (empty = all; only when include_qa is true)." },
|
||||
"min_score": { "type": "number", "default": 0.6, "description": "Drop document chunks below this cosine similarity so off-topic queries surface nothing. Calibrated for the default local BGE embedder (related pairs ~0.75+, unrelated ~0.4-0.5); lower it for a hosted model on a smaller cosine scale. In hybrid mode it is applied to the underlying cosine (not the fused rank). Not applied when rerank is on (see rerank_min_score)." },
|
||||
"announce_empty": { "type": "boolean", "default": false, "x-ui": { "effect": "When nothing relevant is found, inject a note so a grounded agent says it doesn't know instead of guessing." } },
|
||||
"source_filter": { "type": "array", "items": { "type": "string" }, "x-ui": { "widget": "chips", "refType": "kb_source" }, "description": "Restrict retrieval to specific knowledge sources." },
|
||||
"folders": { "type": "array", "items": { "type": "string" }, "x-ui": { "widget": "chips", "refType": "kb_folder" }, "description": "Restrict retrieval to sources in these knowledge folders (empty = all)." },
|
||||
"route_key": { "type": "string", "description": "Optional state key set to 'yes'/'no' (anything found?) so a Router can branch found → agent / not-found → escalation." }
|
||||
},
|
||||
"$comment": "ports: in:text, out:json. Fields mirror forge/nodes/rag.py retrieval_factory."
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/nodes/router",
|
||||
"type": "object",
|
||||
"required": ["expression", "cases"],
|
||||
"properties": {
|
||||
"expression": { "$ref": "forge/common#/$defs/Expression", "x-ui": { "help": "Evaluated over state; result matched against cases." } },
|
||||
"cases": { "type": "object", "additionalProperties": { "type": "string" }, "x-ui": { "widget": "rows-table", "columns": ["value", "target"] } },
|
||||
"default": { "type": "string", "description": "Fallback target node id." },
|
||||
"multi": { "type": "boolean", "default": false, "x-ui": { "effect": "When the expression value is a LIST (multi-label Classifier output), route to EVERY matching case in parallel instead of one." } }
|
||||
},
|
||||
"$comment": "ports: in:any, out: one control port per case + default"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/nodes/start",
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false,
|
||||
"$comment": "ports: out:control"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/nodes/subworkflow",
|
||||
"type": "object",
|
||||
"required": ["workflow_id"],
|
||||
"properties": {
|
||||
"workflow_id": { "type": "string", "x-ui": { "widget": "reference", "refType": "workflow" } },
|
||||
"version": { "type": "integer" },
|
||||
"input_mapping": { "type": "object", "x-ui": { "widget": "rows-table" } },
|
||||
"output_mapping": { "type": "object", "x-ui": { "widget": "rows-table" } }
|
||||
},
|
||||
"$comment": "ports: in:[messages,json], out:[messages,json]"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/nodes/tool_call",
|
||||
"type": "object",
|
||||
"required": ["tool_id"],
|
||||
"properties": {
|
||||
"tool_id": { "type": "string", "x-ui": { "widget": "reference", "refType": "tool" } },
|
||||
"input_mapping": { "type": "object", "description": "Maps state/inputs -> tool args (JMESPath/templates).", "x-ui": { "widget": "rows-table" } },
|
||||
"handle_tool_errors": { "oneOf": [{ "type": "boolean" }, { "type": "string" }], "default": true }
|
||||
},
|
||||
"$comment": "ports: in:json, out:json"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/nodes/transform",
|
||||
"type": "object",
|
||||
"required": ["expression"],
|
||||
"properties": {
|
||||
"engine": { "type": "string", "enum": ["jmespath", "jq"], "default": "jmespath" },
|
||||
"expression": { "type": "string", "x-ui": { "widget": "jmespath", "tokenMeter": false } },
|
||||
"input_key": { "type": "string", "description": "State key to read as input (omit to use whole state)." },
|
||||
"output_key": { "type": "string", "default": "data", "description": "State key to write the result to (must be a declared state field)." }
|
||||
},
|
||||
"$comment": "ports: in:json, out:json"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/nodes/trigger_app_event",
|
||||
"type": "object",
|
||||
"required": ["poll_url"],
|
||||
"properties": {
|
||||
"poll_url": { "type": "string", "x-ui": { "help": "URL polled on each interval; new items fire the workflow." } },
|
||||
"method": { "type": "string", "enum": ["GET", "POST"], "default": "GET" },
|
||||
"interval_minutes": { "type": "integer", "minimum": 1, "default": 5 },
|
||||
"items_path": { "type": "string", "x-ui": { "help": "JMESPath to the list of items in the response (empty = the whole body)." } },
|
||||
"dedupe_key": { "type": "string", "x-ui": { "help": "JMESPath within each item for its unique id (so each item fires once)." } },
|
||||
"message_path": { "type": "string", "x-ui": { "help": "JMESPath within an item for the message text (empty = the whole item as JSON)." } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/nodes/trigger_email",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mailbox": { "type": "string", "x-ui": { "help": "Address/label this trigger handles (informational; routing is per inbound-mailbox config)." } },
|
||||
"include_subject": { "type": "boolean", "default": true },
|
||||
"reply": { "type": "boolean", "default": true, "x-ui": { "help": "Send the workflow's answer back as a threaded email reply." } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/nodes/trigger_schedule",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"every_minutes": { "type": "integer", "minimum": 1, "x-ui": { "help": "Run every N minutes. Use this OR cron." } },
|
||||
"cron": { "type": "string", "x-ui": { "help": "Cron expression (min hour dom mon dow). Requires croniter; otherwise use every_minutes." } },
|
||||
"message": { "type": "string", "x-ui": { "help": "The input message sent into the workflow on each fire." } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/nodes/trigger_webhook",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message_path": { "type": "string", "x-ui": { "help": "JMESPath into the webhook JSON body to use as the user message (empty = the whole body)." } },
|
||||
"require_signature": { "type": "boolean", "default": false, "x-ui": { "help": "Require a valid HMAC-SHA256 signature (X-Forge-Signature) computed with the secret." } },
|
||||
"secret_ref": { "$ref": "forge/common#/$defs/SecretRef" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/nodes/webhook_out",
|
||||
"type": "object",
|
||||
"required": ["url", "method"],
|
||||
"properties": {
|
||||
"url": { "$ref": "forge/common#/$defs/TemplateString" },
|
||||
"method": { "type": "string", "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"] },
|
||||
"auth_provider_id": { "type": "string", "x-ui": { "widget": "reference", "refType": "auth_provider" } },
|
||||
"body": { "type": "object" },
|
||||
"headers": { "type": "object" },
|
||||
"output_key": { "type": "string", "default": "webhook_result", "description": "State key to write the response to (must be a declared state field)." }
|
||||
},
|
||||
"$comment": "ports: in:json, out:json"
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/project",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"default_model": { "$ref": "forge/common#/$defs/ModelRef" },
|
||||
"allowed_models": { "type": "array", "items": { "$ref": "forge/common#/$defs/ModelRef" }, "x-ui": { "widget": "chips" } },
|
||||
"provider_credentials": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "$ref": "forge/common#/$defs/SecretRef" },
|
||||
"description": "provider -> secret ref, e.g. {openai:'secret://proj/openai_key'}"
|
||||
},
|
||||
"budgets": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"max_usd_per_run": { "type": "number" },
|
||||
"max_tokens_per_run": { "type": "integer" },
|
||||
"monthly_usd_cap": { "type": "number" },
|
||||
"alert_at_fraction": { "type": "number", "default": 0.8 }
|
||||
}
|
||||
},
|
||||
"default_middleware": { "type": "array", "items": { "$ref": "forge/middleware#/$defs/MiddlewareEntry" }, "x-ui": { "widget": "middleware-stack" } },
|
||||
"data_region": { "type": "string" },
|
||||
"rag_defaults": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"embedding_model": { "type": "string", "default": "fastembed:BAAI/bge-small-en-v1.5", "x-ui": { "effect": "Embedder for RAG. Default 'fastembed:BAAI/bge-small-en-v1.5' is local, open-source, and free (runs offline). 'openai:text-embedding-3-small|large' is billed per token at ingest AND per query. Changing the model changes the vector dimension, so existing sources must be re-embedded." } },
|
||||
"chunk_size": { "type": "integer", "default": 1000 },
|
||||
"chunk_overlap": { "type": "integer", "default": 200 },
|
||||
"chunking_strategy": { "type": "string", "enum": ["recursive", "section", "sentence", "semantic"], "default": "recursive", "x-ui": { "effect": "Default chunking when a source doesn't specify one: recursive (general), section (Markdown headings), sentence (sentence-aware), semantic (splits where meaning shifts - uses the embedder, slower to ingest)." } },
|
||||
"retrieval_mode": { "type": "string", "enum": ["chunk", "parent_child"], "default": "chunk", "x-ui": { "effect": "chunk: search and return the same chunks (default). parent_child: embed small child chunks for precise matching but feed the agent the larger parent passage for context. Changing this requires re-ingesting existing sources." } },
|
||||
"child_chunk_size": { "type": "integer", "default": 300, "description": "Size (chars) of the small child chunks that get embedded in parent_child mode. The parent window uses chunk_size. Ignored in chunk mode." },
|
||||
"reranker_model": { "type": "string", "default": "Xenova/ms-marco-MiniLM-L-6-v2", "x-ui": { "effect": "Local cross-encoder used when a retrieval node has rerank on. Default 'Xenova/ms-marco-MiniLM-L-6-v2' is small and CPU-fast; 'BAAI/bge-reranker-base' is heavier and more accurate. Runs offline, no API cost." } }
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code_nodes": { "type": "boolean", "default": false },
|
||||
"remote_sandbox": { "type": "boolean", "default": false },
|
||||
"advanced_scripts": { "type": "boolean", "default": false }
|
||||
}
|
||||
},
|
||||
"tracing": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"retention_days": { "type": "integer", "default": 30 },
|
||||
"langfuse_otlp_url": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/tool",
|
||||
"type": "object",
|
||||
"required": ["name", "description", "kind"],
|
||||
"properties": {
|
||||
"name": { "$ref": "forge/common#/$defs/Identifier", "x-ui": { "section": "Identity", "modelView": false } },
|
||||
"display_name": { "type": "string", "x-ui": { "section": "Identity", "modelView": false, "help": "Human-readable name shown in chat/streaming activity. The model still calls the tool by its identifier above; leave blank to show the identifier." } },
|
||||
"description": { "type": "string", "x-ui": { "section": "Identity", "widget": "textarea", "modelView": true, "help": "This is what the model reads to decide when to use the tool." } },
|
||||
"kind": { "type": "string", "enum": ["rest_api", "graphql", "code", "sql", "builtin", "mcp"], "x-ui": { "section": "Identity", "widget": "segmented" } },
|
||||
"auth_provider_id": { "type": "string", "x-ui": { "section": "Auth", "widget": "reference", "refType": "auth_provider" } },
|
||||
"rate_limit": { "type": "object", "properties": { "per_minute": { "type": "integer" } }, "x-ui": { "section": "Reliability" } },
|
||||
"timeout_seconds": { "type": "integer", "default": 30, "x-ui": { "section": "Reliability" } },
|
||||
"retry": { "$ref": "forge/common#/$defs/RetryPolicy", "x-ui": { "section": "Reliability", "advanced": true } },
|
||||
"cache": { "type": "object", "properties": { "ttl_seconds": { "type": "integer", "default": 0 } }, "x-ui": { "section": "Reliability", "advanced": true } }
|
||||
},
|
||||
"allOf": [
|
||||
{ "if": { "properties": { "kind": { "const": "rest_api" } } }, "then": { "$ref": "#/$defs/RestConfig" } },
|
||||
{ "if": { "properties": { "kind": { "const": "graphql" } } }, "then": { "$ref": "#/$defs/GraphQLConfig" } },
|
||||
{ "if": { "properties": { "kind": { "const": "code" } } }, "then": { "$ref": "#/$defs/CodeConfig" } },
|
||||
{ "if": { "properties": { "kind": { "const": "sql" } } }, "then": { "$ref": "#/$defs/SqlConfig" } },
|
||||
{ "if": { "properties": { "kind": { "const": "builtin" } } }, "then": { "$ref": "#/$defs/BuiltinConfig" } }
|
||||
],
|
||||
"$defs": {
|
||||
"FieldSpec": {
|
||||
"type": "object",
|
||||
"required": ["path", "type", "in"],
|
||||
"properties": {
|
||||
"path": { "type": "string", "x-ui": { "widget": "mono" } },
|
||||
"type": { "type": "string", "enum": ["string", "integer", "number", "boolean", "object", "array"] },
|
||||
"in": { "type": "string", "enum": ["path", "query", "header", "body", "cookie"] },
|
||||
"required": { "type": "boolean", "default": false },
|
||||
"llm_visible": { "type": "boolean", "default": true, "x-ui": { "effect": "Model can set this argument" } },
|
||||
"description": { "type": "string", "x-ui": { "modelView": true } },
|
||||
"default": {}
|
||||
}
|
||||
},
|
||||
"ResponseFieldSpec": {
|
||||
"type": "object",
|
||||
"required": ["path"],
|
||||
"properties": {
|
||||
"path": { "type": "string", "x-ui": { "widget": "mono" } },
|
||||
"description": { "type": "string", "x-ui": { "modelView": true } },
|
||||
"include_in_llm": { "type": "boolean", "default": true }
|
||||
}
|
||||
},
|
||||
"RestConfig": {
|
||||
"required": ["request"],
|
||||
"properties": {
|
||||
"request": {
|
||||
"type": "object",
|
||||
"required": ["method", "url_template"],
|
||||
"x-ui": { "section": "Request" },
|
||||
"properties": {
|
||||
"method": { "type": "string", "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"] },
|
||||
"url_template": { "$ref": "forge/common#/$defs/TemplateString" },
|
||||
"body_encoding": { "type": "string", "enum": ["json", "form", "multipart", "raw"], "x-ui": { "section": "Request", "help": "How the request body is encoded. Unset (auto): a structured body is JSON, a body template is sent as-is. json: application/json. form: application/x-www-form-urlencoded — in:body fields (and JSON body templates) are URL-encoded and the Content-Type is set for you; use this for classic HTML form posts, including multi-line values and repeated keys. multipart: multipart/form-data — in:body fields become form parts (a value shaped [filename, content] or {content, filename?, content_type?} is sent as a file part); the Content-Type + boundary are set for you. raw: send the body template string verbatim." } },
|
||||
"body_template": { "$ref": "forge/common#/$defs/TemplateString", "x-ui": { "section": "Request", "widget": "textarea", "help": "Free-form body interpolating {{input.*}} (validated tool args) and {{ctx.*}} (run context). Parsed as JSON when possible, else sent as raw content. For form posts prefer in:body fields with body_encoding=form (auto URL-encoded)." } },
|
||||
"fields": { "type": "array", "items": { "$ref": "#/$defs/FieldSpec" }, "x-ui": { "widget": "rows-table" } },
|
||||
"headers": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "value": { "$ref": "forge/common#/$defs/TemplateString" } } }, "x-ui": { "widget": "rows-table" } },
|
||||
"follow_redirects": { "type": "boolean", "default": false, "x-ui": { "help": "Follow 3xx redirects to the target URL. Each hop is re-checked by the SSRF guard. When off, the redirect's target URL is still reported to the model so it can act on it." } },
|
||||
"tls_skip_verify": { "type": "boolean", "default": false, "x-ui": { "help": "Skip TLS certificate verification. Honored ONLY when the target host is on the server's FORGE_EGRESS_ALLOW_PRIVATE_HOSTS allow-list (a trusted internal/dev target); ignored for any other host. Use for internal services with self-signed certs - never for public endpoints." } }
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"type": "object",
|
||||
"x-ui": { "section": "Response shaping" },
|
||||
"properties": {
|
||||
"fields": { "type": "array", "items": { "$ref": "#/$defs/ResponseFieldSpec" }, "x-ui": { "widget": "rows-table" } },
|
||||
"projection_jmespath": { "type": "string", "x-ui": { "widget": "jmespath", "tokenMeter": true, "help": "Cut the payload before it reaches the model. Compare Raw vs Projected tokens." } }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"GraphQLConfig": {
|
||||
"required": ["endpoint", "query"],
|
||||
"properties": {
|
||||
"endpoint": { "type": "string" },
|
||||
"query": { "type": "string", "x-ui": { "widget": "code", "language": "graphql" } },
|
||||
"operation_name": { "type": "string", "x-ui": { "section": "Request", "help": "operationName to run when the query document defines more than one operation. Leave blank for a single-operation document." } },
|
||||
"variables": { "type": "array", "items": { "$ref": "#/$defs/FieldSpec" }, "x-ui": { "widget": "rows-table" } },
|
||||
"follow_redirects": { "type": "boolean", "default": false, "x-ui": { "help": "Follow 3xx redirects to the target endpoint (each hop re-checked by the SSRF guard)." } },
|
||||
"tls_skip_verify": { "type": "boolean", "default": false, "x-ui": { "help": "Skip TLS certificate verification. Honored ONLY when the endpoint host is on the server's FORGE_EGRESS_ALLOW_PRIVATE_HOSTS allow-list (a trusted internal/dev target); ignored otherwise. Use for internal services with self-signed certs - never for public endpoints." } },
|
||||
"response": { "$ref": "#/$defs/RestConfig/properties/response" }
|
||||
}
|
||||
},
|
||||
"CodeConfig": {
|
||||
"required": ["language", "source"],
|
||||
"properties": {
|
||||
"language": { "type": "string", "enum": ["python"], "default": "python" },
|
||||
"source": { "type": "string", "x-ui": { "widget": "code", "language": "python", "help": "Define def main(**kwargs): return ... (RestrictedPython sandbox; imports limited to pure stdlib)." } },
|
||||
"args_schema": { "type": "object", "description": "JSON Schema of the tool's LLM-visible args." },
|
||||
"timeout_seconds": { "type": "integer", "default": 5 }
|
||||
}
|
||||
},
|
||||
"SqlConfig": {
|
||||
"required": ["query", "connection_ref"],
|
||||
"properties": {
|
||||
"connection_ref": { "type": "string", "x-ui": { "widget": "reference", "refType": "secret", "help": "Secret holding the SQLAlchemy DB URL (e.g. postgresql+psycopg://…)." } },
|
||||
"query": { "type": "string", "x-ui": { "widget": "code", "language": "sql", "help": "Parameterized SELECT using :named binds. Read-only is enforced by default." } },
|
||||
"args_schema": { "type": "object", "description": "JSON Schema of the query's :named parameters (the LLM-visible args)." },
|
||||
"read_only": { "type": "boolean", "default": true },
|
||||
"max_rows": { "type": "integer", "default": 100 }
|
||||
}
|
||||
},
|
||||
"BuiltinConfig": {
|
||||
"required": ["builtin"],
|
||||
"properties": {
|
||||
"builtin": { "type": "string", "enum": ["web_search", "web_fetch", "current_time", "calculator", "knowledge_search", "remember", "recall"] },
|
||||
"options": { "type": "object" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "forge/workflow",
|
||||
"type": "object",
|
||||
"required": ["id", "version", "state", "entry_node", "nodes", "edges"],
|
||||
"properties": {
|
||||
"id": { "$ref": "forge/common#/$defs/Identifier" },
|
||||
"version": { "type": "integer", "minimum": 1 },
|
||||
"state": {
|
||||
"type": "object",
|
||||
"description": "Map of state field name -> StateFieldSpec. Compiled to a TypedDict (NOT pydantic).",
|
||||
"additionalProperties": { "$ref": "#/$defs/StateFieldSpec" },
|
||||
"x-ui": { "widget": "rows-table", "section": "State schema", "columns": ["__key__", "type", "reducer"] }
|
||||
},
|
||||
"entry_node": { "type": "string" },
|
||||
"global_middleware": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "forge/middleware#/$defs/MiddlewareEntry" },
|
||||
"description": "Prepended to every agent node's stack at compile time.",
|
||||
"x-ui": { "widget": "middleware-stack", "section": "Global middleware" }
|
||||
},
|
||||
"error_policy": { "type": "string", "enum": ["halt", "continue", "route_to_error_node"], "default": "halt" },
|
||||
"on_error": {
|
||||
"type": "object",
|
||||
"description": "Graceful fallback when the run errors: send this message instead of failing silently, and optionally open a human handoff.",
|
||||
"properties": {
|
||||
"message": { "type": "string", "x-ui": { "help": "Customer-facing fallback reply when the workflow errors." } },
|
||||
"escalate": { "type": "boolean", "default": false, "x-ui": { "help": "Also open a human-handoff queue item on error." } }
|
||||
}
|
||||
},
|
||||
"timeout_seconds": { "type": "integer", "default": 600 },
|
||||
"max_concurrency": { "type": "integer", "default": 8 },
|
||||
"nodes": { "type": "array", "items": { "$ref": "#/$defs/NodeInstance" }, "minItems": 1 },
|
||||
"edges": { "type": "array", "items": { "$ref": "#/$defs/Edge" } }
|
||||
},
|
||||
"$defs": {
|
||||
"StateFieldSpec": {
|
||||
"type": "object",
|
||||
"required": ["type", "reducer"],
|
||||
"properties": {
|
||||
"type": { "type": "string", "enum": ["list[message]", "list[str]", "list[json]", "str", "int", "float", "bool", "json"] },
|
||||
"reducer": {
|
||||
"type": "string",
|
||||
"enum": ["add_messages", "add", "last", "merge"],
|
||||
"description": "add_messages for messages; add for accumulating lists; last for overwrite; merge for dict."
|
||||
}
|
||||
}
|
||||
},
|
||||
"NodeInstance": {
|
||||
"type": "object",
|
||||
"required": ["id", "type", "config"],
|
||||
"properties": {
|
||||
"id": { "$ref": "forge/common#/$defs/Identifier" },
|
||||
"type": { "type": "string", "description": "Must exist in the Node Type Registry." },
|
||||
"config": { "type": "object", "description": "Validated against the node type's own schema (Section 3)." },
|
||||
"position": { "type": "object", "properties": { "x": { "type": "number" }, "y": { "type": "number" } } }
|
||||
}
|
||||
},
|
||||
"Edge": {
|
||||
"type": "object",
|
||||
"required": ["source", "target"],
|
||||
"properties": {
|
||||
"source": { "type": "string" },
|
||||
"source_handle": { "type": "string" },
|
||||
"target": { "type": "string", "description": "Target node id or 'END'." },
|
||||
"target_handle": { "type": "string" },
|
||||
"condition": { "$ref": "forge/common#/$defs/Expression", "description": "If present, becomes a conditional edge." },
|
||||
"branches": { "type": "object", "additionalProperties": { "type": "string" }, "description": "value -> target-node-id map for routers." }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user