Skip to content

Commit cf72085

Browse files
authored
Merge pull request #84 from AdjectiveAllison/allison/eng-1229-when-creating-tasks-allow-for-usermessage-or-contextwindow
Task Creation Enhancement: Support for ContextWindow (or userMessage)
2 parents 69608e7 + e1aa7e7 commit cf72085

File tree

13 files changed

+443
-61
lines changed

13 files changed

+443
-61
lines changed

acp/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,11 @@ The Tool resource defines a capability that can be used by an Agent, such as:
202202

203203
### Task
204204

205-
The Task resource represents a request to an Agent, which starts a conversation.
205+
The Task resource represents a request to an Agent, which starts a conversation. Tasks can be created in two ways:
206+
- Using a simple `userMessage` for single-turn queries
207+
- Using a `contextWindow` containing multiple messages for multi-turn conversations or continuing previous chats
208+
209+
Only one of these methods can be used per Task.
206210

207211
## License
208212

acp/api/v1alpha1/task_types.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,39 @@ import (
44
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
55
)
66

7+
// Message roles
8+
const (
9+
MessageRoleSystem = "system"
10+
MessageRoleUser = "user"
11+
MessageRoleAssistant = "assistant"
12+
MessageRoleTool = "tool"
13+
)
14+
15+
// ValidMessageRoles provides a map of valid message roles for validation
16+
var ValidMessageRoles = map[string]bool{
17+
MessageRoleSystem: true,
18+
MessageRoleUser: true,
19+
MessageRoleAssistant: true,
20+
MessageRoleTool: true,
21+
}
22+
723
// TaskSpec defines the desired state of Task
824
type TaskSpec struct {
925
// AgentRef references the agent that will execute this Task.
1026
// +kubebuilder:validation:Required
1127
AgentRef LocalObjectReference `json:"agentRef"`
1228

1329
// UserMessage is the message to send to the agent.
14-
// +kubebuilder:validation:Required
15-
// +kubebuilder:validation:MinLength=1
16-
UserMessage string `json:"userMessage"`
30+
// If provided, userMessage will be used and contextWindow must be empty.
31+
// +optional
32+
UserMessage string `json:"userMessage,omitempty"`
33+
34+
// ContextWindow provides the initial conversation context when creating a Task.
35+
// If provided, contextWindow will be used and userMessage must be empty.
36+
// This will be copied to status.ContextWindow, which is the source of truth
37+
// for the ongoing conversation.
38+
// +optional
39+
ContextWindow []Message `json:"contextWindow,omitempty"`
1740
}
1841

1942
// Message represents a single message in the conversation

acp/api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

acp/config/crd/bases/acp.humanlayer.dev_tasks.yaml

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,81 @@ spec:
8585
required:
8686
- name
8787
type: object
88+
contextWindow:
89+
description: |-
90+
ContextWindow provides the initial conversation context when creating a Task.
91+
If provided, contextWindow will be used and userMessage must be empty.
92+
This will be copied to status.ContextWindow, which is the source of truth
93+
for the ongoing conversation.
94+
items:
95+
description: Message represents a single message in the conversation
96+
properties:
97+
content:
98+
description: Content is the message content
99+
type: string
100+
name:
101+
description: Name is the name of the tool that was called
102+
type: string
103+
role:
104+
description: Role is the role of the message sender (system,
105+
user, assistant, tool)
106+
enum:
107+
- system
108+
- user
109+
- assistant
110+
- tool
111+
type: string
112+
toolCallId:
113+
description: ToolCallID is the unique identifier for this tool
114+
call
115+
type: string
116+
toolCalls:
117+
description: ToolCalls contains any tool calls requested by
118+
this message
119+
items:
120+
description: ToolCall represents a request to call a tool
121+
properties:
122+
function:
123+
description: Function contains the details of the function
124+
to call
125+
properties:
126+
arguments:
127+
description: Arguments contains the arguments to pass
128+
to the function in JSON format
129+
type: string
130+
name:
131+
description: Name is the name of the function to call
132+
type: string
133+
required:
134+
- arguments
135+
- name
136+
type: object
137+
id:
138+
description: ID is the unique identifier for this tool
139+
call
140+
type: string
141+
type:
142+
description: Type indicates the type of tool call. Currently
143+
only "function" is supported.
144+
type: string
145+
required:
146+
- function
147+
- id
148+
- type
149+
type: object
150+
type: array
151+
required:
152+
- content
153+
- role
154+
type: object
155+
type: array
88156
userMessage:
89-
description: UserMessage is the message to send to the agent.
90-
minLength: 1
157+
description: |-
158+
UserMessage is the message to send to the agent.
159+
If provided, userMessage will be used and contextWindow must be empty.
91160
type: string
92161
required:
93162
- agentRef
94-
- userMessage
95163
type: object
96164
status:
97165
description: TaskStatus defines the observed state of Task

acp/config/example-resources.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,24 @@ See the samples file for examples of all supported providers.
156156

157157
- **agentRef:**
158158
- References an existing Agent (e.g. `"calculator-agent"`)
159-
- **message:**
159+
- **userMessage:**
160160
- The task prompt or request (e.g. `"What is 2 + 2?"`)
161+
- Used for single-turn queries
162+
- Cannot be used with `contextWindow`
163+
164+
**OR**
165+
166+
- **contextWindow:**
167+
- Array of messages representing an initial conversation
168+
- Used for multi-turn conversations or continuing previous conversations
169+
- Must contain at least one user message (can be at any position)
170+
- Cannot be used with `userMessage`
171+
- If no system message is included, the agent's system message will be prepended
172+
- Tool calls or tool messages in the contextWindow are NOT executed - they're treated purely as conversation history
173+
- The last message can be any valid role (user, assistant, tool) - the controller will send the entire context to the LLM
174+
- The last user message (not necessarily the last message) will be used for the UserMsgPreview field
175+
176+
**Sample with contextWindow:** `config/samples/acp_v1alpha1_context_window_task.yaml`
161177

162178
---
163179

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apiVersion: acp.humanlayer.dev/v1alpha1
2+
kind: Task
3+
metadata:
4+
name: context-window-example
5+
spec:
6+
agentRef:
7+
name: web-fetch-agent
8+
contextWindow:
9+
- role: system
10+
content: "You are a helpful web assistant."
11+
- role: user
12+
content: "Can you search the web for info on climate change?"
13+
- role: assistant
14+
content: "I'll help you research climate change. What specific aspects interest you?"
15+
- role: user
16+
content: "Tell me about recent developments in renewable energy."

acp/docs/crd-reference.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,16 @@ The Task CRD represents a task instance.
138138
| Field | Type | Description | Required |
139139
|-------|------|-------------|----------|
140140
| `agentRef` | LocalObjectReference | Reference to the agent to execute the task | Yes |
141-
| `userMessage` | string | Message to send to the agent | Yes |
141+
| `userMessage` | string | Message to send to the agent | No* |
142+
| `contextWindow` | []Message | Initial conversation context with multiple messages | No* |
143+
144+
*Either `userMessage` or `contextWindow` must be specified, but not both.
142145

143146
### Status Fields
144147

145148
| Field | Type | Description |
146149
|-------|------|-------------|
147150
| `phase` | string | Current phase of execution |
148151
| `phaseHistory` | []PhaseTransition | History of phase transitions |
149-
| `contextWindow` | []Message | The conversation context |
152+
| `contextWindow` | []Message | The conversation context |
153+
| `userMsgPreview` | string | Preview of the user message or last user message in contextWindow |

acp/internal/controller/task/task_controller.go

Lines changed: 46 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/humanlayer/agentcontrolplane/acp/internal/adapters"
2323
"github.com/humanlayer/agentcontrolplane/acp/internal/llmclient"
2424
"github.com/humanlayer/agentcontrolplane/acp/internal/mcpmanager"
25+
"github.com/humanlayer/agentcontrolplane/acp/internal/validation"
2526
"go.opentelemetry.io/otel/attribute"
2627
"go.opentelemetry.io/otel/codes"
2728
"go.opentelemetry.io/otel/trace"
@@ -152,54 +153,63 @@ func (r *TaskReconciler) validateTaskAndAgent(ctx context.Context, task *acp.Tas
152153
return &agent, ctrl.Result{}, nil
153154
}
154155

156+
// Helper function for setting validation errors
157+
func (r *TaskReconciler) setValidationError(ctx context.Context, task *acp.Task, statusUpdate *acp.Task, err error) (ctrl.Result, error) {
158+
logger := log.FromContext(ctx)
159+
logger.Error(err, "Validation failed")
160+
statusUpdate.Status.Ready = false
161+
statusUpdate.Status.Status = acp.TaskStatusTypeError
162+
statusUpdate.Status.Phase = acp.TaskPhaseFailed
163+
statusUpdate.Status.StatusDetail = err.Error()
164+
statusUpdate.Status.Error = err.Error()
165+
r.recorder.Event(task, corev1.EventTypeWarning, "ValidationFailed", err.Error())
166+
if updateErr := r.Status().Update(ctx, statusUpdate); updateErr != nil {
167+
logger.Error(updateErr, "Failed to update Task status")
168+
return ctrl.Result{}, updateErr
169+
}
170+
return ctrl.Result{}, err
171+
}
172+
155173
// prepareForLLM sets up the initial state of a Task with the correct context and phase
156174
func (r *TaskReconciler) prepareForLLM(ctx context.Context, task *acp.Task, statusUpdate *acp.Task, agent *acp.Agent) (ctrl.Result, error) {
157175
logger := log.FromContext(ctx)
158176

159-
// If we're in Initializing or Pending phase, transition to ReadyForLLM
160177
if statusUpdate.Status.Phase == acp.TaskPhaseInitializing || statusUpdate.Status.Phase == acp.TaskPhasePending {
161-
statusUpdate.Status.Phase = acp.TaskPhaseReadyForLLM
162-
statusUpdate.Status.Ready = true
163-
164-
if task.Spec.UserMessage == "" {
165-
err := fmt.Errorf("userMessage is required")
166-
logger.Error(err, "Missing message")
167-
statusUpdate.Status.Ready = false
168-
statusUpdate.Status.Status = acp.TaskStatusTypeError
169-
statusUpdate.Status.Phase = acp.TaskPhaseFailed
170-
statusUpdate.Status.StatusDetail = err.Error()
171-
statusUpdate.Status.Error = err.Error()
172-
r.recorder.Event(task, corev1.EventTypeWarning, "ValidationFailed", err.Error())
173-
if updateErr := r.Status().Update(ctx, statusUpdate); updateErr != nil {
174-
logger.Error(updateErr, "Failed to update Task status")
175-
return ctrl.Result{}, updateErr
176-
}
177-
return ctrl.Result{}, err
178+
if err := validation.ValidateTaskMessageInput(task.Spec.UserMessage, task.Spec.ContextWindow); err != nil {
179+
return r.setValidationError(ctx, task, statusUpdate, err)
178180
}
179181

180-
// Set the UserMsgPreview - truncate to 50 chars if needed
181-
preview := task.Spec.UserMessage
182-
if len(preview) > 50 {
183-
preview = preview[:47] + "..."
182+
var initialContextWindow []acp.Message
183+
if len(task.Spec.ContextWindow) > 0 {
184+
initialContextWindow = append([]acp.Message{}, task.Spec.ContextWindow...)
185+
hasSystemMessage := false
186+
for _, msg := range initialContextWindow {
187+
if msg.Role == acp.MessageRoleSystem {
188+
hasSystemMessage = true
189+
break
190+
}
191+
}
192+
if !hasSystemMessage {
193+
initialContextWindow = append([]acp.Message{
194+
{Role: acp.MessageRoleSystem, Content: agent.Spec.System},
195+
}, initialContextWindow...)
196+
}
197+
} else {
198+
initialContextWindow = []acp.Message{
199+
{Role: acp.MessageRoleSystem, Content: agent.Spec.System},
200+
{Role: acp.MessageRoleUser, Content: task.Spec.UserMessage},
201+
}
184202
}
185-
statusUpdate.Status.UserMsgPreview = preview
186203

187-
// Set up the context window
188-
statusUpdate.Status.ContextWindow = []acp.Message{
189-
{
190-
Role: "system",
191-
Content: agent.Spec.System,
192-
},
193-
{
194-
Role: "user",
195-
Content: task.Spec.UserMessage,
196-
},
197-
}
204+
statusUpdate.Status.UserMsgPreview = validation.GetUserMessagePreview(task.Spec.UserMessage, task.Spec.ContextWindow)
205+
statusUpdate.Status.ContextWindow = initialContextWindow
206+
statusUpdate.Status.Phase = acp.TaskPhaseReadyForLLM
207+
statusUpdate.Status.Ready = true
198208
statusUpdate.Status.Status = acp.TaskStatusTypeReady
199209
statusUpdate.Status.StatusDetail = "Ready to send to LLM"
200-
statusUpdate.Status.Error = "" // Clear previous error
201-
r.recorder.Event(task, corev1.EventTypeNormal, "ValidationSucceeded", "Task validation succeeded")
210+
statusUpdate.Status.Error = ""
202211

212+
r.recorder.Event(task, corev1.EventTypeNormal, "ValidationSucceeded", "Task validation succeeded")
203213
if err := r.Status().Update(ctx, statusUpdate); err != nil {
204214
logger.Error(err, "Failed to update Task status")
205215
return ctrl.Result{}, err

0 commit comments

Comments
 (0)