@stigmer/mcp-server 3.6.0 → 3.7.0
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/cli/mcp-server-stigmer.js +5 -3
- package/domains/channels/tools.d.ts.map +1 -1
- package/domains/channels/tools.js +3 -1
- package/domains/channels/tools.js.map +1 -1
- package/domains/conversation/calls.d.ts +9 -0
- package/domains/conversation/calls.d.ts.map +1 -0
- package/domains/conversation/calls.js +30 -0
- package/domains/conversation/calls.js.map +1 -0
- package/domains/conversation/errors.d.ts +19 -0
- package/domains/conversation/errors.d.ts.map +1 -0
- package/domains/conversation/errors.js +73 -0
- package/domains/conversation/errors.js.map +1 -0
- package/domains/conversation/tools.d.ts +13 -0
- package/domains/conversation/tools.d.ts.map +1 -0
- package/domains/conversation/tools.js +69 -0
- package/domains/conversation/tools.js.map +1 -0
- package/gen/workflow.d.ts +154 -61
- package/gen/workflow.d.ts.map +1 -1
- package/gen/workflow.js +98 -46
- package/gen/workflow.js.map +1 -1
- package/index.d.ts +1 -1
- package/index.d.ts.map +1 -1
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/package.json +3 -3
- package/server.d.ts +40 -7
- package/server.d.ts.map +1 -1
- package/server.js +58 -5
- package/server.js.map +1 -1
- package/src/domains/channels/tools.ts +5 -1
- package/src/domains/conversation/calls.ts +41 -0
- package/src/domains/conversation/conversation.integration.test.ts +233 -0
- package/src/domains/conversation/errors.ts +88 -0
- package/src/domains/conversation/tools.ts +87 -0
- package/src/gen/workflow.ts +99 -40
- package/src/http.integration.test.ts +56 -2
- package/src/index.ts +11 -1
- package/src/server.ts +66 -9
package/src/gen/workflow.ts
CHANGED
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
import { generateSlug, visibilityFromString, enumFromString, toTimestamp } from "./apply-runtime.js";
|
|
7
7
|
import { create, fromJson, toJson, type JsonObject, type JsonValue } from "@bufbuild/protobuf";
|
|
8
8
|
import { ValueSchema } from "@bufbuild/protobuf/wkt";
|
|
9
|
-
import {
|
|
9
|
+
import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
10
|
+
import { RunConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/invocation_pb";
|
|
10
11
|
import { EnvVarDeclarationSchema } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/spec_pb";
|
|
11
|
-
import { Harness } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
|
|
12
|
+
import { GitWriteBackMode, Harness } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
|
|
13
|
+
import { GitRepoSourceSchema, LocalPathSourceSchema, WorkspaceSourceSchema, WorkspaceEntrySchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/workspace_pb";
|
|
12
14
|
import { WorkflowSchema, type Workflow } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
|
|
13
15
|
import { WorkflowTaskKind, BudgetExceededPolicy } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/enum_pb";
|
|
14
16
|
import { WorkflowSpecSchema, WorkflowDocumentSchema, ExportSchema, FlowControlSchema, WorkflowTaskSchema, WorkflowBudgetSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/spec_pb";
|
|
15
|
-
import {
|
|
17
|
+
import { AgentCallOutputContractSchema, AgentCallTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/agent_call_pb";
|
|
16
18
|
import { CallActivityTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/call_activity_pb";
|
|
17
19
|
import { OnInvalidOutputPolicy } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/common_pb";
|
|
18
20
|
import { EmitEventSpecSchema, EmitEventTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/emit_event_pb";
|
|
@@ -33,6 +35,8 @@ import { TransformTaskConfigSchema, TransformEngine } from "@stigmer/protos/ai/s
|
|
|
33
35
|
import { CatchBlockSchema, TryTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/try_pb";
|
|
34
36
|
import { ValidationRuleSchema, ValidateTaskConfigSchema, ValidationFailPolicy } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/validate_pb";
|
|
35
37
|
import { DurationSchema, WaitTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/wait_pb";
|
|
38
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
39
|
+
import { ApiResourceReferenceSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
36
40
|
import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
|
|
37
41
|
import { z } from "zod";
|
|
38
42
|
|
|
@@ -63,21 +67,13 @@ const WorkflowDocumentInputSchema = z.object({
|
|
|
63
67
|
});
|
|
64
68
|
type WorkflowDocumentInput = z.infer<typeof WorkflowDocumentInputSchema>;
|
|
65
69
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
const RunConfigInputSchema = z.object({
|
|
71
|
+
model_name: z.string().optional().describe("The model each run uses. Example: 'claude-sonnet-4-6'."),
|
|
72
|
+
max_cost_usd: z.number().optional().describe("Maximum estimated cost in USD per run. The surface's platform execution profile caps this value; the lower bound wins."),
|
|
73
|
+
max_tool_rounds: z.number().optional().describe("Maximum model-to-tools reasoning cycles per run. The surface's platform execution profile caps this value; the lower bound wins. @internal An implementation knob, not a user concept: API-reachable for operators, deliberately absent from creation forms (DD-018 D-5)."),
|
|
74
|
+
service_tier: z.string().optional().describe("Service tier for each run's model calls: standard (the default) or fast, where fast bills at the model's fast-tier rates and requires a model that offers one. In workflow YAML the shorthand spellings 'standard'/'fast' are accepted alongside the canonical enum names. Mirrors ExecutionConfig.service_tier: UNSPECIFIED inherits the surface's platform default, which itself resolves to STANDARD — never the provider account default. FAST requires model_name (here or from the platform profile) to name a model with a registry fast pricing variant; validated fail-closed at create. Allowed values: SERVICE_TIER_STANDARD, SERVICE_TIER_FAST."),
|
|
70
75
|
});
|
|
71
|
-
type
|
|
72
|
-
|
|
73
|
-
const AgentExecutionConfigInputSchema = z.object({
|
|
74
|
-
model: z.string().optional().describe("LLM model to use for this invocation. Example: 'claude-3-5-sonnet', 'gpt-4', 'claude-3-opus' Optional - uses agent's default model if not specified."),
|
|
75
|
-
timeout: z.number().optional().describe("Timeout for agent execution in seconds. Default: 300 (5 minutes) Optional."),
|
|
76
|
-
temperature: z.number().optional().describe("Temperature for LLM sampling (0.0 to 1.0). Lower = more deterministic, Higher = more creative Default: 0.7 Optional."),
|
|
77
|
-
context_management: z.lazy(() => ContextManagementConfigInputSchema).optional().describe("Context management configuration for this agent invocation. Controls automatic summarization behavior for long-running conversations. When specified, overrides model defaults from the Model Registry. @internal @since Phase 3 (Context Summarization Architecture) Use cases: - Disable summarization for short-lived agents - Custom thresholds for agents with specific context requirements - Fine-tune summarization behavior per workflow task Example YAML: config: model: 'claude-sonnet-4.5' context_management: custom_trigger_threshold: 150000 custom_target_tokens: 120000"),
|
|
78
|
-
max_cost_micros: z.union([z.number(), z.string()]).optional().describe("Per-agent-call cost cap in micro-USD (1 USD = 1,000,000 micros). When set, the runtime terminates this agent call if its accumulated cost exceeds this limit. This uses the workflow domain's micro-USD convention and provides per-task cost control at the workflow level. The runtime checks both: per-task limit first, then workflow remaining budget. Optional — when 0, no per-task cost limit is enforced. @since T05 (Workflow-Level Budget Primitives)"),
|
|
79
|
-
});
|
|
80
|
-
type AgentExecutionConfigInput = z.infer<typeof AgentExecutionConfigInputSchema>;
|
|
76
|
+
type RunConfigInput = z.infer<typeof RunConfigInputSchema>;
|
|
81
77
|
|
|
82
78
|
const AgentCallOutputContractInputSchema = z.object({
|
|
83
79
|
schema: z.record(z.unknown()).describe("JSON Schema that the agent's structured output must conform to. Standard JSON Schema (draft 2020-12 or compatible). The workflow runner extracts JSON from the agent's final response and validates it against this schema. If validation fails, the on_invalid policy determines what happens next. The schema is carried as a google.protobuf.Struct to preserve the existing kind+Struct envelope pattern and allow YAML authors to write standard JSON Schema inline without a Stigmer-specific schema language."),
|
|
@@ -87,14 +83,47 @@ const AgentCallOutputContractInputSchema = z.object({
|
|
|
87
83
|
});
|
|
88
84
|
type AgentCallOutputContractInput = z.infer<typeof AgentCallOutputContractInputSchema>;
|
|
89
85
|
|
|
86
|
+
const GitRepoSourceInputSchema = z.object({
|
|
87
|
+
url: z.string().describe("HTTPS clone URL for the repository."),
|
|
88
|
+
branch: z.string().optional().describe("Branch to clone. When empty, the repository's default branch is used."),
|
|
89
|
+
commit: z.string().optional().describe("Commit SHA to checkout after cloning. When set, the workspace is checked out at this exact commit. When both branch and commit are set, the branch is cloned first, then the commit is checked out."),
|
|
90
|
+
depth: z.number().optional().describe("Number of commits to include in the clone history. When not set, defaults to a shallow clone with depth 1. Set to 0 for a full clone with complete history. @internal Uses proto3 optional to distinguish 'not set' from 'set to 0.' Absent: shallow clone depth 1; 0: full clone; N > 0: shallow clone depth N."),
|
|
91
|
+
write_back_mode: z.string().optional().describe("Controls whether the platform creates a branch and pull request from the agent's file changes. @internal This is a platform-level workflow, not an agent-level decision. The agent focuses on making code changes; the platform packages them incrementally — the PR appears the moment the first file is written and the diff grows in real time as the agent works. Requires GITHUB_TOKEN in the execution environment. If credentials are not available, the write-back is silently skipped regardless of this setting. Default (UNSPECIFIED): platform decides. Currently defaults to write-back enabled when git credentials are available. Allowed values: GIT_WRITE_BACK_BRANCH_AND_PR."),
|
|
92
|
+
});
|
|
93
|
+
type GitRepoSourceInput = z.infer<typeof GitRepoSourceInputSchema>;
|
|
94
|
+
|
|
95
|
+
const LocalPathSourceInputSchema = z.object({
|
|
96
|
+
path: z.string().optional().describe("Absolute path to an existing directory on the host filesystem."),
|
|
97
|
+
});
|
|
98
|
+
type LocalPathSourceInput = z.infer<typeof LocalPathSourceInputSchema>;
|
|
99
|
+
|
|
100
|
+
const WorkspaceSourceInputSchema = z.object({
|
|
101
|
+
git_repo: z.lazy(() => GitRepoSourceInputSchema).optional().describe("Clone a git repository as the workspace source."),
|
|
102
|
+
local_path: z.lazy(() => LocalPathSourceInputSchema).optional().describe("Use an existing local directory as the workspace source."),
|
|
103
|
+
});
|
|
104
|
+
type WorkspaceSourceInput = z.infer<typeof WorkspaceSourceInputSchema>;
|
|
105
|
+
|
|
106
|
+
const WorkspaceEntryInputSchema = z.object({
|
|
107
|
+
name: z.string().optional().describe("Short identifier for this workspace entry."),
|
|
108
|
+
source: z.lazy(() => WorkspaceSourceInputSchema).describe("The source that provides this entry's content."),
|
|
109
|
+
});
|
|
110
|
+
type WorkspaceEntryInput = z.infer<typeof WorkspaceEntryInputSchema>;
|
|
111
|
+
|
|
112
|
+
const EnvironmentRefInputSchema = z.object({
|
|
113
|
+
org: z.string().optional().describe("Organization that owns the referenced resource. When non-empty: must be a valid org slug (lowercase alphanumeric with hyphens, starts with a letter, 1-63 characters). Example: 'stigmer', 'acme-corp'. When empty: the reference is relative — the server resolves it to the parent resource's organization at write time. All stored and returned references always have org populated (absolute form). Use empty org for same-org references (the common case). Use explicit org for cross-org references (e.g., marketplace resources)."),
|
|
114
|
+
slug: z.string().describe("Resource slug (user-friendly identifier, unique within org). Format: lowercase alphanumeric with hyphens, must start with a letter and end with a letter or digit (e.g., 'web-search', 'code-reviewer'). Length: 2-63 characters."),
|
|
115
|
+
});
|
|
116
|
+
type EnvironmentRefInput = z.infer<typeof EnvironmentRefInputSchema>;
|
|
117
|
+
|
|
90
118
|
const AgentCallTaskConfigInputSchema = z.object({
|
|
91
|
-
agent: z.string().describe("Agent reference in 'org/slug' or 'slug' format. - 'slug' only: uses the workflow's organization - 'org/slug': explicit organization reference Examples: 'code-reviewer', 'stigmer/code-reviewer', 'acme/data-analyst' Required field."),
|
|
92
|
-
org: z.string().optional().describe("Explicit organization for agent resolution. Optional. If empty, the org is parsed from the agent field or defaults to workflow's org. Use this when you need to override the parsed org."),
|
|
119
|
+
agent: z.string().describe("Agent reference in 'org/slug' or 'slug' format. - 'slug' only: uses the workflow's organization - 'org/slug': explicit organization reference Examples: 'code-reviewer', 'stigmer/code-reviewer', 'acme/data-analyst' Required field. @internal The DSL string form of AgentInvocation.agent_ref. The structured ApiResourceReference shape is deliberately not used here: the authoring surface is YAML written by hand, and 'org/slug' is its idiom."),
|
|
93
120
|
message: z.string().describe("Instructions/prompt to send to the agent. Supports interpolation of workflow variables using JQ expressions. Example: 'Analyze this code: ${ $context.fetchCode.body }' Required field."),
|
|
94
121
|
env: z.record(z.string()).optional().describe("Runtime environment variables to pass to the agent. Values can be literal strings or JQ expressions that reference workflow context or secrets. Example: {'GITHUB_TOKEN': '${ .secrets.GH_TOKEN }'} Optional."),
|
|
95
|
-
|
|
96
|
-
output: z.lazy(() => AgentCallOutputContractInputSchema).optional().describe("Structured output contract for this agent call. When set, the workflow runner extracts structured JSON from the agent's final response and validates it against the declared schema. The validated JSON is placed in the task output under the 'structured' key, enabling reliable downstream routing via switch_case expressions. When not set, the task output contains the agent's raw text response
|
|
97
|
-
harness: z.string().optional().describe("Execution harness for the agent invocation. Determines which execution engine processes the agent call: - HARNESS_UNSPECIFIED / HARNESS_NATIVE: Stigmer native engine (Python/LangGraph) - HARNESS_CURSOR: Cursor SDK engine (TypeScript/Cursor) The workflow-runner creates a Session with this harness before creating the AgentExecution. The harness is a session-level concern — it determines tool availability, state management, model access, and billing tier. When unspecified, defaults to HARNESS_NATIVE
|
|
122
|
+
run_config: z.lazy(() => RunConfigInputSchema).optional().describe("Per-call model choice and run bounds. Unset fields inherit the platform defaults. @internal The shared RunConfig (DD-018 D-2) — the same message schedules embed, so the run-bound vocabulary cannot drift between triggering surfaces. Semantics at the workflow surface: model_name replaces the agent's default outright; max_cost_usd maps to ExecutionConfig.max_cost_usd and is enforced by the runner's harness-generic cost guards; max_tool_rounds maps to ExecutionConfig.max_tool_rounds (native harness only — inert on cursor, whose sole bound is cost). No platform clamp profile is applied at this surface yet: per the RunConfig contract, an unset platform cap means the owner value stands."),
|
|
123
|
+
output: z.lazy(() => AgentCallOutputContractInputSchema).optional().describe("Structured output contract for this agent call. When set, the workflow runner extracts structured JSON from the agent's final response and validates it against the declared schema. The validated JSON is placed in the task output under the 'structured' key, enabling reliable downstream routing via switch_case expressions. When not set, the task output contains the agent's raw text response. @since T02 (Structured Agent Output Model)"),
|
|
124
|
+
harness: z.string().optional().describe("Execution harness for the agent invocation. Determines which execution engine processes the agent call: - HARNESS_UNSPECIFIED / HARNESS_NATIVE: Stigmer native engine (Python/LangGraph) - HARNESS_CURSOR: Cursor SDK engine (TypeScript/Cursor) The workflow-runner creates a Session with this harness before creating the AgentExecution. The harness is a session-level concern — it determines tool availability, state management, model access, and billing tier. When unspecified, defaults to HARNESS_NATIVE (the workflow surface's platform default). YAML Example: - code_review: call: agent with: agent: 'code-reviewer' harness: cursor message: 'Review this PR' Allowed values: HARNESS_NATIVE, HARNESS_CURSOR."),
|
|
125
|
+
workspace_entries: z.array(z.lazy(() => WorkspaceEntryInputSchema)).optional().describe("Workspace the child run's session operates on. Empty means no workspace. @internal The shared WorkspaceEntry type (AgentInvocation correspondence). Maps onto SessionSpec.workspace_entries of the session each call creates. Surface constraint, enforced in workflow validation (both editions): sources must be git_repo — no client is connected to serve a local_path when a workflow task fires. Credentials follow DD-018 D-4: the provisioner resolves GITHUB_TOKEN from the merged environment; for private repos the supported contract is an org-visibility Environment holding GITHUB_TOKEN bound via environment_refs. Public repos need no token."),
|
|
126
|
+
environment_refs: z.array(z.lazy(() => EnvironmentRefInputSchema)).optional().describe("References to Environment resources whose values are provided to the child runs this task creates. This is how a tool-using agent becomes runnable from a workflow: bind an org-shared environment holding the needed credentials, and the child runs receive its values at runtime. The agent and its default instance stay untouched. @internal The AgentShare/AgentChannel/Schedule environment_refs lineage. Never carried in the execution create request and never emitted into execution YAML: CreateExecutionContextStep resolves them from the Workflow row, gated on the trusted runner caller identity and keyed by parent_workflow_id + task label — prepended at LOWEST merge priority (instance refs and runtime_env override on key conflicts). No write-time existence or visibility check: enforcement lives solely at runtime resolution, which fails closed. When kind is unset in YAML, the DSL normalizer defaults it to environment."),
|
|
98
127
|
});
|
|
99
128
|
type AgentCallTaskConfigInput = z.infer<typeof AgentCallTaskConfigInputSchema>;
|
|
100
129
|
|
|
@@ -384,7 +413,7 @@ interface WorkflowTaskInput {
|
|
|
384
413
|
const WorkflowTaskInputSchema: z.ZodType<WorkflowTaskInput> = z.lazy(() => z.object({
|
|
385
414
|
name: z.string().optional().describe("Task name/identifier (must be unique within workflow). Must be a valid C-style identifier: starts with a letter or underscore, followed by letters, digits, or underscores."),
|
|
386
415
|
kind: z.string().describe("Task type. Set the matching config field (e.g. kind='http_call' -> populate http_call). Allowed values: set_vars, http_call, grpc_call, activity_call, switch_case, for_each, fork, try_catch, listen, wait, raise_error, run_workflow, agent_call, llm_call, transform, human_input, validate, emit_event, notification, eval."),
|
|
387
|
-
agent_call: z.lazy(() => AgentCallTaskConfigInputSchema).optional().describe("Required when kind='agent_call'. AgentCallTaskConfig defines the configuration for agent_call tasks that invoke AI agents. @internal
|
|
416
|
+
agent_call: z.lazy(() => AgentCallTaskConfigInputSchema).optional().describe("Required when kind='agent_call'. AgentCallTaskConfig defines the configuration for agent_call tasks that invoke AI agents. @internal This message is the workflow DSL's adoption of the shared AgentInvocation vocabulary (agentexecution/v1/invocation.proto) at the TYPE level — issue stigmer/stigmer#358, per DD-018 (whatsapp-proactive-messaging, stigmer-cloud). Because WorkflowTask.task_config is a 'kind + Struct' envelope, this message IS the authoring schema: its field names are the YAML keys workflow authors write. Embedding AgentInvocation as a nested message would force either a nested 'invocation:' authoring block or flatten/unflatten rewrites in every Struct consumer, so the DSL stays flat and shares the vocabulary type-by-type instead. Field-by-field correspondence to AgentInvocation (keep in lockstep; a field added there must be consciously adopted or consciously excluded here, with the reason recorded): - agent ↔ agent_ref — the DSL string form ('org/slug'); the workflow runner parses it into an agent reference. - message ↔ message — expression-carrying in the DSL. - harness ↔ harness — same shared enum, same semantics. - run_config ↔ run_config — the shared message, embedded directly. - workspace_entries ↔ workspace_entries — same shared type; git sources only (no client is connected to serve a local_path when a workflow task fires). - environment_refs ↔ environment_refs — same shared type; resolved server-side at execution create, never carried in the create request (the schedule/channel/share posture). Surface-specific fields with their reasons (the DD-017/018 bucket discipline): - env: the workflow's context-forwarding channel. Values are JQ expressions resolved from workflow state/secrets at run time, not plaintext literals in a manifest — which is why AgentInvocation's runtime_env exclusion does not apply to it. - output: the structured-output contract is a workflow-routing concern (switch_case on typed fields), meaningless to other invocation surfaces. Deleted in the #358 clean break (no reserved numbers; task configs are stored as JSON Structs, so field numbers never hit a wire): - org: redundant with the 'org/slug' form of 'agent', and the proto→YAML converter never emitted it. - AgentExecutionConfig (timeout, temperature, context_management, max_cost_micros): declared knobs the runtime silently ignored. The cost cap lives on run_config.max_cost_usd and is now actually enforced; timeout/temperature had no runtime counterpart at all. The agent is referenced by org/slug format (e.g., 'stigmer/code-reviewer'). Resolution order: 1. 'org/slug': look in that org's agents 2. 'slug' only: use the workflow's org 3. Before external lookup, check manifest (current deployment) YAML Example (without structured output): - analyze: call: agent with: agent: 'code-reviewer' message: 'Review this code: ${ $context.fetchCode.body }' env: GITHUB_TOKEN: '${ .secrets.GH_TOKEN }' run_config: model_name: 'claude-sonnet-4-6' max_cost_usd: 0.50 YAML Example (with structured output): - triage_ticket: call: agent with: agent: 'support-triage' message: '${ .ticket.description }' output: schema: type: object required: [severity, category, customer_impact] properties: severity: type: string enum: [low, medium, high, critical] category: type: string customer_impact: type: boolean rationale: type: string on_invalid: ON_INVALID_RETRY max_retries: 2 fallback_task: human_review export: as: '${ .structured }'"),
|
|
388
417
|
activity_call: z.lazy(() => CallActivityTaskConfigInputSchema).optional().describe("Required when kind='activity_call'. CallActivityTaskConfig defines the configuration for activity_call tasks that execute activities. @internal Executes Temporal activities. YAML Example: - taskName: call: activity with: activity: 'ProcessDataActivity' input: data: ${ .data } Reference: zigflow-dsl-pattern-catalog.md - Task Type 10"),
|
|
389
418
|
emit_event: z.lazy(() => EmitEventTaskConfigInputSchema).optional().describe("Required when kind='emit_event'. EmitEventTaskConfig defines the configuration for emit_event tasks that publish CloudEvents to external consumers, other workflows, audit trails, and metrics systems. @internal emit_event is the complement to listen. While listen waits for Temporal signals (internal workflow primitives), emit_event publishes business events using the CloudEvents envelope (a standard external contract). The runtime (T13) bridges the two: an emitted CloudEvent can be delivered as a Temporal signal to another workflow's listen task, or routed to external consumers via message queues, webhooks, or event buses. The task output is the fully-resolved CloudEvents envelope with runtime-generated fields populated: { 'id': '<generated UUID>', 'specversion': '1.0', 'type': 'stigmer.workflow.ticket.classified', 'source': '/workflows/triage/executions/abc-123', 'time': '2026-05-12T14:30:00Z', 'subject': 'TICKET-456', 'datacontenttype': 'application/json', 'data': { <resolved payload> } } YAML Example (emit a classification event): - notify_classified: emit_event: event: type: 'stigmer.workflow.ticket.classified' subject: '${ $context.ticket.id }' data: ticket_id: '${ $context.ticket.id }' severity: '${ $context.triage.severity }' category: '${ $context.triage.category }' classified_by: '${ $context.workflow_instance_id }' export: as: '${ . }' YAML Example (emit with explicit source): - publish_completion: emit_event: event: type: 'acme.order.fulfilled' source: 'urn:acme:fulfillment-service' subject: '${ $context.order.number }' data: order_id: '${ $context.order.id }' status: 'fulfilled' fulfilled_at: '${ $context.timestamp }' export: as: '${ . }'"),
|
|
390
419
|
eval: z.lazy(() => EvalTaskConfigInputSchema).optional().describe("Required when kind='eval'. EvalTaskConfig defines the configuration for eval tasks that use an LLM judge to assess the semantic quality of workflow data. @internal Use eval when you need to assess quality, correctness, safety, or completeness of LLM-generated or agent-produced content. This fills the gap between structural validation (validate task — JSON Schema, business rules) and human review (human_input task — manual inspection). The eval task constructs a judge prompt from the rubric and subject, calls the specified LLM with structured output enforcement, parses the judge's response, and applies the threshold to determine pass/fail. Key differences from validate: - validate checks deterministic structural properties (schema, rules) - eval checks semantic quality via an LLM judge (hallucination, relevance, safety) Key differences from llm_call: - llm_call is a general-purpose LLM invocation - eval has built-in judge prompt construction, scoring semantics, threshold application, and on_fail branching Task output structure: { 'pass': true/false, 'score': 0.85, // present for numeric_score and multi_criteria 'reasoning': 'The response...', 'criteria': [ // present only for multi_criteria {'name': 'accuracy', 'score': 0.9, 'reasoning': '...'}, {'name': 'safety', 'score': 0.8, 'reasoning': '...'} ], 'model_used': 'gpt-4o', 'subject': <original subject data> } YAML Example (binary pass/fail evaluation): - check_summary_quality: call: eval with: model: 'gpt-4o' subject: '${ $context.summarize.text }' rubric: | Evaluate whether this summary accurately captures the key points of the source document without hallucinations or omissions. The summary should be concise (under 3 sentences) and factually correct. scoring_mode: EVAL_PASS_FAIL on_fail: EVAL_FAIL_BRANCH fallback_task: regenerate_summary export: as: '${ . }' YAML Example (numeric score with threshold): - score_translation: call: eval with: model: 'gpt-4o' subject: '${ $context.translate.text }' rubric: | Rate the quality of this translation on accuracy, fluency, and preservation of meaning. Score 0.0 (unusable) to 1.0 (perfect). scoring_mode: EVAL_NUMERIC_SCORE threshold: 0.7 on_fail: EVAL_FAIL_WARN export: as: '${ . }' YAML Example (multi-criteria evaluation): - evaluate_response: call: eval with: model: 'gpt-4o' subject: '${ $context.agent_response }' rubric: 'Evaluate this customer support response.' scoring_mode: EVAL_MULTI_CRITERIA threshold: 0.75 criteria: - name: accuracy description: 'Is the information factually correct?' weight: 3.0 - name: helpfulness description: 'Does it address the customer's actual question?' weight: 2.0 - name: tone description: 'Is the tone professional and empathetic?' weight: 1.0 on_fail: EVAL_FAIL_RAISE export: as: '${ . }'"),
|
|
@@ -461,21 +490,12 @@ function workflowDocumentInputToProto(input: WorkflowDocumentInput) {
|
|
|
461
490
|
return result;
|
|
462
491
|
}
|
|
463
492
|
|
|
464
|
-
function
|
|
465
|
-
const result = create(
|
|
466
|
-
if (input.
|
|
467
|
-
if (input.
|
|
468
|
-
if (input.
|
|
469
|
-
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function agentExecutionConfigInputToProto(input: AgentExecutionConfigInput) {
|
|
473
|
-
const result = create(AgentExecutionConfigSchema);
|
|
474
|
-
if (input.model !== undefined) result.model = input.model;
|
|
475
|
-
if (input.timeout !== undefined) result.timeout = input.timeout;
|
|
476
|
-
if (input.temperature !== undefined) result.temperature = input.temperature;
|
|
477
|
-
if (input.context_management !== undefined) result.contextManagement = contextManagementConfigInputToProto(input.context_management);
|
|
478
|
-
if (input.max_cost_micros !== undefined) result.maxCostMicros = BigInt(input.max_cost_micros);
|
|
493
|
+
function runConfigInputToProto(input: RunConfigInput) {
|
|
494
|
+
const result = create(RunConfigSchema);
|
|
495
|
+
if (input.model_name !== undefined) result.modelName = input.model_name;
|
|
496
|
+
if (input.max_cost_usd !== undefined) result.maxCostUsd = input.max_cost_usd;
|
|
497
|
+
if (input.max_tool_rounds !== undefined) result.maxToolRounds = input.max_tool_rounds;
|
|
498
|
+
result.serviceTier = enumFromString(ServiceTier, input.service_tier) as ServiceTier;
|
|
479
499
|
return result;
|
|
480
500
|
}
|
|
481
501
|
|
|
@@ -488,15 +508,54 @@ function agentCallOutputContractInputToProto(input: AgentCallOutputContractInput
|
|
|
488
508
|
return result;
|
|
489
509
|
}
|
|
490
510
|
|
|
511
|
+
function gitRepoSourceInputToProto(input: GitRepoSourceInput) {
|
|
512
|
+
const result = create(GitRepoSourceSchema);
|
|
513
|
+
if (input.url !== undefined) result.url = input.url;
|
|
514
|
+
if (input.branch !== undefined) result.branch = input.branch;
|
|
515
|
+
if (input.commit !== undefined) result.commit = input.commit;
|
|
516
|
+
if (input.depth !== undefined) result.depth = input.depth;
|
|
517
|
+
result.writeBackMode = enumFromString(GitWriteBackMode, input.write_back_mode) as GitWriteBackMode;
|
|
518
|
+
return result;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function localPathSourceInputToProto(input: LocalPathSourceInput) {
|
|
522
|
+
const result = create(LocalPathSourceSchema);
|
|
523
|
+
if (input.path !== undefined) result.path = input.path;
|
|
524
|
+
return result;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function workspaceSourceInputToProto(input: WorkspaceSourceInput) {
|
|
528
|
+
const result = create(WorkspaceSourceSchema);
|
|
529
|
+
if (input.git_repo !== undefined) result.source = { case: "gitRepo", value: gitRepoSourceInputToProto(input.git_repo) };
|
|
530
|
+
if (input.local_path !== undefined) result.source = { case: "localPath", value: localPathSourceInputToProto(input.local_path) };
|
|
531
|
+
return result;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function workspaceEntryInputToProto(input: WorkspaceEntryInput) {
|
|
535
|
+
const result = create(WorkspaceEntrySchema);
|
|
536
|
+
if (input.name !== undefined) result.name = input.name;
|
|
537
|
+
if (input.source !== undefined) result.source = workspaceSourceInputToProto(input.source);
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function environmentRefInputToProto(input: EnvironmentRefInput) {
|
|
542
|
+
return create(ApiResourceReferenceSchema, {
|
|
543
|
+
org: input.org,
|
|
544
|
+
slug: input.slug,
|
|
545
|
+
kind: ApiResourceKind.environment,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
491
549
|
function agentCallTaskConfigInputToProto(input: AgentCallTaskConfigInput) {
|
|
492
550
|
const result = create(AgentCallTaskConfigSchema);
|
|
493
551
|
if (input.agent !== undefined) result.agent = input.agent;
|
|
494
|
-
if (input.org !== undefined) result.org = input.org;
|
|
495
552
|
if (input.message !== undefined) result.message = input.message;
|
|
496
553
|
if (input.env !== undefined) result.env = input.env;
|
|
497
|
-
if (input.
|
|
554
|
+
if (input.run_config !== undefined) result.runConfig = runConfigInputToProto(input.run_config);
|
|
498
555
|
if (input.output !== undefined) result.output = agentCallOutputContractInputToProto(input.output);
|
|
499
556
|
result.harness = enumFromString(Harness, input.harness) as Harness;
|
|
557
|
+
if (input.workspace_entries !== undefined) result.workspaceEntries = input.workspace_entries.map(workspaceEntryInputToProto);
|
|
558
|
+
if (input.environment_refs !== undefined) result.environmentRefs = input.environment_refs.map(environmentRefInputToProto);
|
|
500
559
|
return result;
|
|
501
560
|
}
|
|
502
561
|
|
|
@@ -12,7 +12,7 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
|
12
12
|
|
|
13
13
|
import type { Config } from "./config";
|
|
14
14
|
import { configureLogger } from "./logger";
|
|
15
|
-
import {
|
|
15
|
+
import { routedServerFactory, serveHttp } from "./server";
|
|
16
16
|
|
|
17
17
|
configureLogger({ level: "error", format: "text" });
|
|
18
18
|
|
|
@@ -49,7 +49,14 @@ beforeAll(async () => {
|
|
|
49
49
|
logLevel: "error",
|
|
50
50
|
};
|
|
51
51
|
controller = new AbortController();
|
|
52
|
-
|
|
52
|
+
// The REAL route dispatch, so these tests hold the production route
|
|
53
|
+
// table: full roster on "/", records on "/records", channels on
|
|
54
|
+
// "/channels", 404 anywhere else.
|
|
55
|
+
serving = serveHttp(
|
|
56
|
+
routedServerFactory({ serverAddress: cfg.stigmerServerAddress, apiKey: "" }),
|
|
57
|
+
cfg,
|
|
58
|
+
controller.signal,
|
|
59
|
+
);
|
|
53
60
|
// Give the listener a tick to bind.
|
|
54
61
|
await new Promise((r) => setTimeout(r, 100));
|
|
55
62
|
});
|
|
@@ -139,3 +146,50 @@ describe("HTTP transport hardening + OAuth discovery", () => {
|
|
|
139
146
|
expect(await res.text()).toContain("missing Mcp-Session-Id header");
|
|
140
147
|
});
|
|
141
148
|
});
|
|
149
|
+
|
|
150
|
+
/** POST a real MCP initialize to `path`; returns the raw Response. */
|
|
151
|
+
function initialize(path: string): Promise<Response> {
|
|
152
|
+
return fetch(`${base()}${path}`, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: {
|
|
155
|
+
authorization: "Bearer test-token",
|
|
156
|
+
"content-type": "application/json",
|
|
157
|
+
accept: "application/json, text/event-stream",
|
|
158
|
+
},
|
|
159
|
+
body: JSON.stringify({
|
|
160
|
+
jsonrpc: "2.0",
|
|
161
|
+
id: 1,
|
|
162
|
+
method: "initialize",
|
|
163
|
+
params: {
|
|
164
|
+
protocolVersion: "2025-03-26",
|
|
165
|
+
capabilities: {},
|
|
166
|
+
clientInfo: { name: "http-integration", version: "test" },
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
describe("HTTP route dispatch (the closed route table)", () => {
|
|
173
|
+
it.each([
|
|
174
|
+
["/", "mcp-server-stigmer"],
|
|
175
|
+
["/records", "mcp-server-stigmer-records"],
|
|
176
|
+
["/channels", "mcp-server-stigmer-channels"],
|
|
177
|
+
["/conversation", "mcp-server-stigmer-conversation"],
|
|
178
|
+
])("serves the %s roster as %s", async (path, serverName) => {
|
|
179
|
+
const res = await initialize(path);
|
|
180
|
+
expect(res.status).toBe(200);
|
|
181
|
+
// The initialize result rides an SSE frame; the serverInfo name is
|
|
182
|
+
// the roster's identity and must match the route exactly.
|
|
183
|
+
expect(await res.text()).toContain(`"name":"${serverName}"`);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("refuses an unknown route with 404 instead of a default roster", async () => {
|
|
187
|
+
// The 2026-08-05 incident regression pin: a bridge that does not
|
|
188
|
+
// recognize a route must say so at connect time — silently serving
|
|
189
|
+
// the full roster there once handed an agent every management tool
|
|
190
|
+
// except the send tool its attachment existed for.
|
|
191
|
+
const res = await initialize("/no-such-roster");
|
|
192
|
+
expect(res.status).toBe(404);
|
|
193
|
+
expect(await res.text()).toContain("unknown MCP route: /no-such-roster");
|
|
194
|
+
});
|
|
195
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -18,7 +18,17 @@ import type { BackendTarget } from "./domains/client.js";
|
|
|
18
18
|
|
|
19
19
|
export type { Config, OAuthConfig, Roster, Transport } from "./config.js";
|
|
20
20
|
export { loadConfigFromEnv, validateConfig } from "./config.js";
|
|
21
|
-
export {
|
|
21
|
+
export {
|
|
22
|
+
CHANNELS_ROUTE,
|
|
23
|
+
CONVERSATION_ROUTE,
|
|
24
|
+
createChannelsServer,
|
|
25
|
+
createConversationServer,
|
|
26
|
+
createRecordsServer,
|
|
27
|
+
createServer,
|
|
28
|
+
FULL_ROUTE,
|
|
29
|
+
RECORDS_ROUTE,
|
|
30
|
+
SERVER_VERSION,
|
|
31
|
+
} from "./server.js";
|
|
22
32
|
|
|
23
33
|
/** Returns a Config populated from environment variables (no validation). */
|
|
24
34
|
export function defaultConfig(): Config {
|
package/src/server.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { registerAgentResources } from "./domains/agents/resources.js";
|
|
|
24
24
|
import { registerAgentTools } from "./domains/agents/tools.js";
|
|
25
25
|
import { registerChannelTools } from "./domains/channels/tools.js";
|
|
26
26
|
import type { BackendTarget } from "./domains/client.js";
|
|
27
|
+
import { registerConversationTools } from "./domains/conversation/tools.js";
|
|
27
28
|
import { registerDatastoreResources } from "./domains/datastores/resources.js";
|
|
28
29
|
import { registerDatastoreTools } from "./domains/datastores/tools.js";
|
|
29
30
|
import { registerEnvironmentResources } from "./domains/environments/resources.js";
|
|
@@ -99,6 +100,31 @@ export function createChannelsServer(target: BackendTarget): McpServer {
|
|
|
99
100
|
return server;
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Build a conversation-only MCP server: escalate_to_human with the
|
|
105
|
+
* agent-facing argument surface, and nothing else (channel-conversations
|
|
106
|
+
* DD-008 D-c / A14 — the channels-roster pattern). This is the roster
|
|
107
|
+
* the runner-synthesized conversation attachment connects to; the
|
|
108
|
+
* structural guarantee mirrors the channels roster's.
|
|
109
|
+
*
|
|
110
|
+
* HTTP-only by design — served on the /conversation route, with NO
|
|
111
|
+
* stdio roster value: escalate is cloud-only (OSS refuses
|
|
112
|
+
* FAILED_PRECONDITION) AND session-token-only (the reach derives
|
|
113
|
+
* identity from a session-scoped sandbox credential, which only the
|
|
114
|
+
* bridge's per-request Bearer can carry — a stdio child's startup API
|
|
115
|
+
* key never could), so a stdio shape would be a tool that can only
|
|
116
|
+
* fail. Honest absence instead.
|
|
117
|
+
*/
|
|
118
|
+
export function createConversationServer(target: BackendTarget): McpServer {
|
|
119
|
+
const server = new McpServer({
|
|
120
|
+
name: "mcp-server-stigmer-conversation",
|
|
121
|
+
version: SERVER_VERSION,
|
|
122
|
+
});
|
|
123
|
+
const tools = registerConversationTools(server, target);
|
|
124
|
+
log.info("tools registered (conversation roster)", { count: tools.length, tools });
|
|
125
|
+
return server;
|
|
126
|
+
}
|
|
127
|
+
|
|
102
128
|
/**
|
|
103
129
|
* Wire up every domain's tools. Each domain returns the names it registered so
|
|
104
130
|
* the startup log's count and roster cannot drift from what is actually wired,
|
|
@@ -174,11 +200,16 @@ export type ServerFactory = () => McpServer;
|
|
|
174
200
|
|
|
175
201
|
/**
|
|
176
202
|
* Builds the server for an inbound HTTP `initialize` request, selected
|
|
177
|
-
* by request path
|
|
178
|
-
*
|
|
179
|
-
*
|
|
203
|
+
* by request path; `undefined` means the route is not recognized and the
|
|
204
|
+
* request must be refused (404), never served a default roster. Only the
|
|
205
|
+
* initialize request consults the path — an established session's
|
|
206
|
+
* transport already carries the server it was built with, so follow-up
|
|
207
|
+
* requests dispatch by Mcp-Session-Id alone.
|
|
180
208
|
*/
|
|
181
|
-
export type RouteServerFactory = (path: string) => McpServer;
|
|
209
|
+
export type RouteServerFactory = (path: string) => McpServer | undefined;
|
|
210
|
+
|
|
211
|
+
/** HTTP route serving the full roster: the bare origin every published config uses. */
|
|
212
|
+
export const FULL_ROUTE = "/";
|
|
182
213
|
|
|
183
214
|
/** HTTP route serving the records-only roster (T05 R1). */
|
|
184
215
|
export const RECORDS_ROUTE = "/records";
|
|
@@ -186,16 +217,32 @@ export const RECORDS_ROUTE = "/records";
|
|
|
186
217
|
/** HTTP route serving the channels-only roster (DD-006 D8). */
|
|
187
218
|
export const CHANNELS_ROUTE = "/channels";
|
|
188
219
|
|
|
220
|
+
/** HTTP route serving the conversation-only roster (channel-conversations A14). */
|
|
221
|
+
export const CONVERSATION_ROUTE = "/conversation";
|
|
222
|
+
|
|
189
223
|
/**
|
|
190
|
-
* The standard HTTP route dispatch: the
|
|
191
|
-
* {@link RECORDS_ROUTE}, the channels-only
|
|
192
|
-
* {@link CHANNELS_ROUTE}, the
|
|
224
|
+
* The standard HTTP route dispatch: the full roster on {@link FULL_ROUTE},
|
|
225
|
+
* the records-only roster on {@link RECORDS_ROUTE}, the channels-only
|
|
226
|
+
* roster on {@link CHANNELS_ROUTE}, the conversation-only roster on
|
|
227
|
+
* {@link CONVERSATION_ROUTE} — and NOTHING anywhere else.
|
|
228
|
+
*
|
|
229
|
+
* The closed route table is load-bearing, not tidiness. This dispatch
|
|
230
|
+
* once fell through to the full roster for any unrecognized path, and a
|
|
231
|
+
* production bridge predating /channels answered the runner's channel
|
|
232
|
+
* attachment with the full roster: the agent got a server named
|
|
233
|
+
* stigmer-channels with every management tool except the one send tool
|
|
234
|
+
* it existed for (the 2026-08-05 stale-pin incident). An unknown route
|
|
235
|
+
* is a deployment mismatch by definition — refuse it loudly so the
|
|
236
|
+
* mismatch is visible at connect time, instead of serving tools the
|
|
237
|
+
* caller was never meant to see.
|
|
193
238
|
*/
|
|
194
239
|
export function routedServerFactory(target: BackendTarget): RouteServerFactory {
|
|
195
240
|
return (path) => {
|
|
241
|
+
if (path === FULL_ROUTE) return createServer(target);
|
|
196
242
|
if (path === RECORDS_ROUTE) return createRecordsServer(target);
|
|
197
243
|
if (path === CHANNELS_ROUTE) return createChannelsServer(target);
|
|
198
|
-
return
|
|
244
|
+
if (path === CONVERSATION_ROUTE) return createConversationServer(target);
|
|
245
|
+
return undefined;
|
|
199
246
|
};
|
|
200
247
|
}
|
|
201
248
|
|
|
@@ -356,6 +403,16 @@ async function routeRequest(
|
|
|
356
403
|
return;
|
|
357
404
|
}
|
|
358
405
|
|
|
406
|
+
// Route recognition happens exactly where the roster choice happens.
|
|
407
|
+
// An unknown route gets a 404, never a default roster — see
|
|
408
|
+
// routedServerFactory for the incident this prevents.
|
|
409
|
+
const server = makeServer(path);
|
|
410
|
+
if (server === undefined) {
|
|
411
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
412
|
+
res.end(`unknown MCP route: ${path}`);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
359
416
|
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
|
|
360
417
|
sessionIdGenerator: () => randomUUID(),
|
|
361
418
|
onsessioninitialized: (id) => {
|
|
@@ -369,7 +426,7 @@ async function routeRequest(
|
|
|
369
426
|
if (transport.sessionId !== undefined) sessions.delete(transport.sessionId);
|
|
370
427
|
};
|
|
371
428
|
|
|
372
|
-
await
|
|
429
|
+
await server.connect(transport);
|
|
373
430
|
await transport.handleRequest(req, res, body);
|
|
374
431
|
}
|
|
375
432
|
|