///|
pub struct CodingAgentNodeSpec[S, P] {
agent : CodingAgent
resource_key : @core.ResourceKey
resource_scope : @core.ResourceScope
open_context : (@core.NodeContext, S) -> CodingAgentOpenContext raise
build_prompt : (@core.NodeContext, S) -> @cli.Prompt raise
select_continuation : (@core.NodeContext, S) -> CodingAgentContinuation? raise
decode_response : (S, @cli.FinalResponse, CodingAgentContinuation?) -> @core.NodeOutput[
P,
] raise
}
///|
/// Creates the callbacks and resource policy used by a coding-agent node.
pub fn[S, P] CodingAgentNodeSpec::CodingAgentNodeSpec(
agent~ : CodingAgent,
resource_key~ : @core.ResourceKey,
resource_scope~ : @core.ResourceScope,
open_context~ : (@core.NodeContext, S) -> CodingAgentOpenContext raise,
build_prompt~ : (@core.NodeContext, S) -> @cli.Prompt raise,
select_continuation~ : (@core.NodeContext, S) -> CodingAgentContinuation? raise,
decode_response~ : (S, @cli.FinalResponse, CodingAgentContinuation?) -> @core.NodeOutput[
P,
] raise,
) -> CodingAgentNodeSpec[S, P] {
CodingAgentNodeSpec::{
agent,
resource_key,
resource_scope,
open_context,
build_prompt,
select_continuation,
decode_response,
}
}
///|
fn resolve_prompt_context_files(
workspace_root : @path.Path,
prompt : @cli.Prompt,
) -> @cli.Prompt {
@cli.Prompt::Prompt(
prompt.text,
context_files=prompt.context_files.map(path => {
if path.is_absolute() {
path
} else {
workspace_root |> @path.Path::join(path)
}
}),
)
}
///|
priv struct CodingAgentResource {
session : &@cli.CliSession
mutex : @async.Mutex
}
///|
impl Debug for CodingAgentResource with fn to_repr(_self) {
@debug.Repr::literal("CodingAgentResource")
}
///|
extenum @any.Payload += {
CodingAgentResource(CodingAgentResource)
}
///|
impl @any.Anyable for CodingAgentResource with fn iso() {
{
id: @any.TypeId::TypeId(
"totto2727/workgraph-agent-cli",
"CodingAgentResource",
[],
),
box: resource => CodingAgentResource(resource),
unbox: payload => {
guard payload is CodingAgentResource(resource)
resource
},
}
}
///|
fn coding_agent_resource_ref(
key : @core.ResourceKey,
agent_id : CodingAgentId,
) -> @any_collection.AnyRef[@core.ResourceKey, CodingAgentResource] raise {
let agent = agent_id.to_string()
let composite_key = @core.ResourceKey::ResourceKey(
"coding-agent:\{agent.length()}:\{agent}:\{key.to_string()}",
)
@any_collection.AnyRef::AnyRef(composite_key)
}
///|
/// Create a graph node that acquires an agent SDK session, serializes a prompt, and decodes its final response.
pub fn[S, P] coding_agent_node(
id : @core.NodeId,
metadata : @core.NodeMetadata,
spec : CodingAgentNodeSpec[S, P],
) -> @core.Node[S, P] {
@core.Node::Node(id, metadata, async fn(context, state) {
let selected_continuation = (spec.select_continuation)(context, state)
match selected_continuation {
Some(continuation) if continuation.owner != spec.agent.id =>
raise AgentMismatch(expected=spec.agent.id, actual=continuation.owner)
_ => ()
}
let open_context = (spec.open_context)(context, state)
let reference = coding_agent_resource_ref(spec.resource_key, spec.agent.id)
let open_resource = async fn() {
let cli = (spec.agent.open)(open_context)
let session = match selected_continuation {
Some(continuation) => cli.continue_session(continuation.raw)
None => cli.start()
}
CodingAgentResource::{ session, mutex: @async.Mutex::Mutex() }
}
let resource = match spec.resource_scope {
@core.Node =>
context.resources.acquire_resource(
reference,
@core.Node,
open_resource,
fn(_resource) { () },
owner=context.node_id,
)
@core.Run =>
context.resources.acquire_resource(
reference,
@core.Run,
open_resource,
fn(_resource) { () },
)
}
let prompt = resolve_prompt_context_files(
open_context.workspace.root,
(spec.build_prompt)(context, state),
)
resource.mutex.acquire()
defer resource.mutex.release()
let response = resource.session.prompt(prompt)
let continuation = response.continuation.map(raw => {
CodingAgentContinuation::CodingAgentContinuation(spec.agent.id, raw)
})
(spec.decode_response)(state, response, continuation)
})
}